Implement an anonymous account service for looking up accounts
This commit is contained in:
parent
eaa868cf06
commit
601e9eebbd
|
@ -379,7 +379,7 @@ public class AccountController {
|
||||||
)
|
)
|
||||||
@ApiResponse(responseCode = "200", description = "Account found for the given username.", useReturnTypeSchema = true)
|
@ApiResponse(responseCode = "200", description = "Account found for the given username.", useReturnTypeSchema = true)
|
||||||
@ApiResponse(responseCode = "400", description = "Request must not be authenticated.")
|
@ApiResponse(responseCode = "400", description = "Request must not be authenticated.")
|
||||||
@ApiResponse(responseCode = "404", description = "Account not fount for the given username.")
|
@ApiResponse(responseCode = "404", description = "Account not found for the given username.")
|
||||||
public CompletableFuture<AccountIdentifierResponse> lookupUsernameHash(
|
public CompletableFuture<AccountIdentifierResponse> lookupUsernameHash(
|
||||||
@Auth final Optional<AuthenticatedAccount> maybeAuthenticatedAccount,
|
@Auth final Optional<AuthenticatedAccount> maybeAuthenticatedAccount,
|
||||||
@PathParam("usernameHash") final String usernameHash) {
|
@PathParam("usernameHash") final String usernameHash) {
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.grpc;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import org.signal.chat.account.CheckAccountExistenceRequest;
|
||||||
|
import org.signal.chat.account.CheckAccountExistenceResponse;
|
||||||
|
import org.signal.chat.account.LookupUsernameHashRequest;
|
||||||
|
import org.signal.chat.account.LookupUsernameHashResponse;
|
||||||
|
import org.signal.chat.account.LookupUsernameLinkRequest;
|
||||||
|
import org.signal.chat.account.LookupUsernameLinkResponse;
|
||||||
|
import org.signal.chat.account.ReactorAccountsAnonymousGrpc;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||||
|
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class AccountsAnonymousGrpcService extends ReactorAccountsAnonymousGrpc.AccountsAnonymousImplBase {
|
||||||
|
|
||||||
|
private final AccountsManager accountsManager;
|
||||||
|
private final RateLimiters rateLimiters;
|
||||||
|
|
||||||
|
public AccountsAnonymousGrpcService(final AccountsManager accountsManager, final RateLimiters rateLimiters) {
|
||||||
|
this.accountsManager = accountsManager;
|
||||||
|
this.rateLimiters = rateLimiters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<CheckAccountExistenceResponse> checkAccountExistence(final CheckAccountExistenceRequest request) {
|
||||||
|
final ServiceIdentifier serviceIdentifier =
|
||||||
|
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getServiceIdentifier());
|
||||||
|
|
||||||
|
return RateLimitUtil.rateLimitByRemoteAddress(rateLimiters.getCheckAccountExistenceLimiter())
|
||||||
|
.then(Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier)))
|
||||||
|
.map(Optional::isPresent)
|
||||||
|
.map(accountExists -> CheckAccountExistenceResponse.newBuilder()
|
||||||
|
.setAccountExists(accountExists)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<LookupUsernameHashResponse> lookupUsernameHash(final LookupUsernameHashRequest request) {
|
||||||
|
if (request.getUsernameHash().size() != AccountController.USERNAME_HASH_LENGTH) {
|
||||||
|
throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription(String.format("Illegal username hash length; expected %d bytes, but got %d bytes",
|
||||||
|
AccountController.USERNAME_HASH_LENGTH, request.getUsernameHash().size()))
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RateLimitUtil.rateLimitByRemoteAddress(rateLimiters.getUsernameLookupLimiter())
|
||||||
|
.then(Mono.fromFuture(() -> accountsManager.getByUsernameHash(request.getUsernameHash().toByteArray())))
|
||||||
|
.map(maybeAccount -> maybeAccount.orElseThrow(Status.NOT_FOUND::asRuntimeException))
|
||||||
|
.map(account -> LookupUsernameHashResponse.newBuilder()
|
||||||
|
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(account.getUuid())))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<LookupUsernameLinkResponse> lookupUsernameLink(final LookupUsernameLinkRequest request) {
|
||||||
|
final UUID linkHandle;
|
||||||
|
|
||||||
|
try {
|
||||||
|
linkHandle = UUIDUtil.fromByteString(request.getUsernameLinkHandle());
|
||||||
|
} catch (final IllegalArgumentException e) {
|
||||||
|
throw Status.INVALID_ARGUMENT
|
||||||
|
.withDescription("Could not interpret link handle as UUID")
|
||||||
|
.withCause(e)
|
||||||
|
.asRuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RateLimitUtil.rateLimitByRemoteAddress(rateLimiters.getUsernameLinkLookupLimiter())
|
||||||
|
.then(Mono.fromFuture(() -> accountsManager.getByUsernameLinkHandle(linkHandle)))
|
||||||
|
.map(maybeAccount -> maybeAccount
|
||||||
|
.flatMap(Account::getEncryptedUsername)
|
||||||
|
.orElseThrow(Status.NOT_FOUND::asRuntimeException))
|
||||||
|
.map(usernameCiphertext -> LookupUsernameLinkResponse.newBuilder()
|
||||||
|
.setUsernameCiphertext(ByteString.copyFrom(usernameCiphertext))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
}
|
|
@ -163,6 +163,10 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
|
||||||
return forDescriptor(For.USERNAME_LOOKUP);
|
return forDescriptor(For.USERNAME_LOOKUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RateLimiter getUsernameLinkLookupLimiter() {
|
||||||
|
return forDescriptor(For.USERNAME_LINK_LOOKUP_PER_IP);
|
||||||
|
}
|
||||||
|
|
||||||
public RateLimiter getUsernameSetLimiter() {
|
public RateLimiter getUsernameSetLimiter() {
|
||||||
return forDescriptor(For.USERNAME_SET);
|
return forDescriptor(For.USERNAME_SET);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option java_multiple_files = true;
|
||||||
|
|
||||||
|
package org.signal.chat.account;
|
||||||
|
|
||||||
|
import "org/signal/chat/common.proto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides methods for looking up Signal accounts. Callers must not provide
|
||||||
|
* identifying credentials when calling methods in this service.
|
||||||
|
*/
|
||||||
|
service AccountsAnonymous {
|
||||||
|
/**
|
||||||
|
* Checks whether an account with the given service identifier exists.
|
||||||
|
*/
|
||||||
|
rpc CheckAccountExistence(CheckAccountExistenceRequest) returns (CheckAccountExistenceResponse) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the service identifier of the account associated with the given
|
||||||
|
* username hash. This method will return a `NOT_FOUND` status if no account
|
||||||
|
* was found for the given username hash.
|
||||||
|
*/
|
||||||
|
rpc LookupUsernameHash(LookupUsernameHashRequest) returns (LookupUsernameHashResponse) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the encrypted username identified by a given username link handle.
|
||||||
|
* This method will return a `NOT_FOUND` status if no username was found for
|
||||||
|
* the given link handle.
|
||||||
|
*/
|
||||||
|
rpc LookupUsernameLink(LookupUsernameLinkRequest) returns (LookupUsernameLinkResponse) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckAccountExistenceRequest {
|
||||||
|
/**
|
||||||
|
* The service identifier of an account that may or may not exist.
|
||||||
|
*/
|
||||||
|
common.ServiceIdentifier service_identifier = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CheckAccountExistenceResponse {
|
||||||
|
/**
|
||||||
|
* True if an account exists with the given service identifier or false if no
|
||||||
|
* account was found.
|
||||||
|
*/
|
||||||
|
bool account_exists = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LookupUsernameHashRequest {
|
||||||
|
/**
|
||||||
|
* A 32-byte username hash for which to find an account.
|
||||||
|
*/
|
||||||
|
bytes username_hash = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LookupUsernameHashResponse {
|
||||||
|
/**
|
||||||
|
* The service identifier associated with a given username hash.
|
||||||
|
*/
|
||||||
|
common.ServiceIdentifier service_identifier = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LookupUsernameLinkRequest {
|
||||||
|
/**
|
||||||
|
* The link handle for which to find an encrypted username. Link handles are
|
||||||
|
* 16-byte representations of UUIDs.
|
||||||
|
*/
|
||||||
|
bytes username_link_handle = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LookupUsernameLinkResponse {
|
||||||
|
/**
|
||||||
|
* The ciphertext of the username identified by the given link handle.
|
||||||
|
*/
|
||||||
|
bytes username_ciphertext = 1;
|
||||||
|
}
|
|
@ -27,6 +27,12 @@ message ServiceIdentifier {
|
||||||
bytes uuid = 2;
|
bytes uuid = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AccountIdentifiers {
|
||||||
|
repeated ServiceIdentifier service_identifiers = 1;
|
||||||
|
string e164 = 2;
|
||||||
|
bytes username_hash = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message EcPreKey {
|
message EcPreKey {
|
||||||
/**
|
/**
|
||||||
* A locally-unique identifier for this key.
|
* A locally-unique identifier for this key.
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* 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 static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
|
import io.grpc.Status;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
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.MethodSource;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.signal.chat.account.AccountsAnonymousGrpc;
|
||||||
|
import org.signal.chat.account.CheckAccountExistenceRequest;
|
||||||
|
import org.signal.chat.account.LookupUsernameHashRequest;
|
||||||
|
import org.signal.chat.account.LookupUsernameLinkRequest;
|
||||||
|
import org.signal.chat.common.IdentityType;
|
||||||
|
import org.signal.chat.common.ServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||||
|
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||||
|
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||||
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
class AccountsAnonymousGrpcServiceTest extends
|
||||||
|
SimpleBaseGrpcTest<AccountsAnonymousGrpcService, AccountsAnonymousGrpc.AccountsAnonymousBlockingStub> {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AccountsManager accountsManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RateLimiters rateLimiters;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RateLimiter rateLimiter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AccountsAnonymousGrpcService createServiceBeforeEachTest() {
|
||||||
|
when(accountsManager.getByServiceIdentifierAsync(any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||||
|
|
||||||
|
when(accountsManager.getByUsernameHash(any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||||
|
|
||||||
|
when(accountsManager.getByUsernameLinkHandle(any()))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.empty()));
|
||||||
|
|
||||||
|
when(rateLimiters.getCheckAccountExistenceLimiter()).thenReturn(rateLimiter);
|
||||||
|
when(rateLimiters.getUsernameLookupLimiter()).thenReturn(rateLimiter);
|
||||||
|
when(rateLimiters.getUsernameLinkLookupLimiter()).thenReturn(rateLimiter);
|
||||||
|
|
||||||
|
when(rateLimiter.validateReactive(anyString())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
getMockRemoteAddressInterceptor().setRemoteAddress(new InetSocketAddress("127.0.0.1", 12345));
|
||||||
|
|
||||||
|
return new AccountsAnonymousGrpcService(accountsManager, rateLimiters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkAccountExistence() {
|
||||||
|
final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID());
|
||||||
|
|
||||||
|
when(accountsManager.getByServiceIdentifierAsync(serviceIdentifier))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(mock(Account.class))));
|
||||||
|
|
||||||
|
assertTrue(unauthenticatedServiceStub().checkAccountExistence(CheckAccountExistenceRequest.newBuilder()
|
||||||
|
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(serviceIdentifier))
|
||||||
|
.build()).getAccountExists());
|
||||||
|
|
||||||
|
assertFalse(unauthenticatedServiceStub().checkAccountExistence(CheckAccountExistenceRequest.newBuilder()
|
||||||
|
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(UUID.randomUUID())))
|
||||||
|
.build()).getAccountExists());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void checkAccountExistenceIllegalRequest(final CheckAccountExistenceRequest request) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
||||||
|
() -> unauthenticatedServiceStub().checkAccountExistence(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> checkAccountExistenceIllegalRequest() {
|
||||||
|
return Stream.of(
|
||||||
|
// No service identifier
|
||||||
|
Arguments.of(CheckAccountExistenceRequest.newBuilder().build()),
|
||||||
|
|
||||||
|
// Bad service identifier
|
||||||
|
Arguments.of(CheckAccountExistenceRequest.newBuilder()
|
||||||
|
.setServiceIdentifier(ServiceIdentifier.newBuilder()
|
||||||
|
.setIdentityType(IdentityType.IDENTITY_TYPE_ACI)
|
||||||
|
.setUuid(ByteString.copyFrom(new byte[15]))
|
||||||
|
.build())
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkAccountExistenceRateLimited() {
|
||||||
|
final Duration retryAfter = Duration.ofSeconds(11);
|
||||||
|
|
||||||
|
when(rateLimiter.validateReactive(anyString()))
|
||||||
|
.thenReturn(Mono.error(new RateLimitExceededException(retryAfter, false)));
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
|
||||||
|
() -> unauthenticatedServiceStub().checkAccountExistence(CheckAccountExistenceRequest.newBuilder()
|
||||||
|
.setServiceIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(UUID.randomUUID())))
|
||||||
|
.build()),
|
||||||
|
accountsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lookupUsernameHash() {
|
||||||
|
final UUID accountIdentifier = UUID.randomUUID();
|
||||||
|
|
||||||
|
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
|
||||||
|
new SecureRandom().nextBytes(usernameHash);
|
||||||
|
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
when(account.getUuid()).thenReturn(accountIdentifier);
|
||||||
|
|
||||||
|
when(accountsManager.getByUsernameHash(usernameHash))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
|
||||||
|
|
||||||
|
assertEquals(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(accountIdentifier)),
|
||||||
|
unauthenticatedServiceStub().lookupUsernameHash(LookupUsernameHashRequest.newBuilder()
|
||||||
|
.setUsernameHash(ByteString.copyFrom(usernameHash))
|
||||||
|
.build())
|
||||||
|
.getServiceIdentifier());
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertStatusException(Status.NOT_FOUND,
|
||||||
|
() -> unauthenticatedServiceStub().lookupUsernameHash(LookupUsernameHashRequest.newBuilder()
|
||||||
|
.setUsernameHash(ByteString.copyFrom(new byte[AccountController.USERNAME_HASH_LENGTH]))
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void lookupUsernameHashIllegalHash(final LookupUsernameHashRequest request) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
||||||
|
() -> unauthenticatedServiceStub().lookupUsernameHash(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> lookupUsernameHashIllegalHash() {
|
||||||
|
return Stream.of(
|
||||||
|
// No username hash
|
||||||
|
Arguments.of(LookupUsernameHashRequest.newBuilder().build()),
|
||||||
|
|
||||||
|
// Hash too long
|
||||||
|
Arguments.of(LookupUsernameHashRequest.newBuilder()
|
||||||
|
.setUsernameHash(ByteString.copyFrom(new byte[AccountController.USERNAME_HASH_LENGTH + 1]))
|
||||||
|
.build()),
|
||||||
|
|
||||||
|
// Hash too short
|
||||||
|
Arguments.of(LookupUsernameHashRequest.newBuilder()
|
||||||
|
.setUsernameHash(ByteString.copyFrom(new byte[AccountController.USERNAME_HASH_LENGTH - 1]))
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lookupUsernameHashRateLimited() {
|
||||||
|
final Duration retryAfter = Duration.ofSeconds(13);
|
||||||
|
|
||||||
|
when(rateLimiter.validateReactive(anyString()))
|
||||||
|
.thenReturn(Mono.error(new RateLimitExceededException(retryAfter, false)));
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
|
||||||
|
() -> unauthenticatedServiceStub().lookupUsernameHash(LookupUsernameHashRequest.newBuilder()
|
||||||
|
.setUsernameHash(ByteString.copyFrom(new byte[AccountController.USERNAME_HASH_LENGTH]))
|
||||||
|
.build()),
|
||||||
|
accountsManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lookupUsernameLink() {
|
||||||
|
final UUID linkHandle = UUID.randomUUID();
|
||||||
|
|
||||||
|
final byte[] usernameCiphertext = new byte[32];
|
||||||
|
new SecureRandom().nextBytes(usernameCiphertext);
|
||||||
|
|
||||||
|
final Account account = mock(Account.class);
|
||||||
|
when(account.getEncryptedUsername()).thenReturn(Optional.of(usernameCiphertext));
|
||||||
|
|
||||||
|
when(accountsManager.getByUsernameLinkHandle(linkHandle))
|
||||||
|
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
|
||||||
|
|
||||||
|
assertEquals(ByteString.copyFrom(usernameCiphertext),
|
||||||
|
unauthenticatedServiceStub().lookupUsernameLink(LookupUsernameLinkRequest.newBuilder()
|
||||||
|
.setUsernameLinkHandle(UUIDUtil.toByteString(linkHandle))
|
||||||
|
.build())
|
||||||
|
.getUsernameCiphertext());
|
||||||
|
|
||||||
|
when(account.getEncryptedUsername()).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertStatusException(Status.NOT_FOUND,
|
||||||
|
() -> unauthenticatedServiceStub().lookupUsernameLink(LookupUsernameLinkRequest.newBuilder()
|
||||||
|
.setUsernameLinkHandle(UUIDUtil.toByteString(linkHandle))
|
||||||
|
.build()));
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertStatusException(Status.NOT_FOUND,
|
||||||
|
() -> unauthenticatedServiceStub().lookupUsernameLink(LookupUsernameLinkRequest.newBuilder()
|
||||||
|
.setUsernameLinkHandle(UUIDUtil.toByteString(UUID.randomUUID()))
|
||||||
|
.build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource
|
||||||
|
void lookupUsernameLinkIllegalHandle(final LookupUsernameLinkRequest request) {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
|
||||||
|
() -> unauthenticatedServiceStub().lookupUsernameLink(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Arguments> lookupUsernameLinkIllegalHandle() {
|
||||||
|
return Stream.of(
|
||||||
|
// No handle
|
||||||
|
Arguments.of(LookupUsernameLinkRequest.newBuilder().build()),
|
||||||
|
|
||||||
|
// Bad handle length
|
||||||
|
Arguments.of(LookupUsernameLinkRequest.newBuilder()
|
||||||
|
.setUsernameLinkHandle(ByteString.copyFrom(new byte[15]))
|
||||||
|
.build())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lookupUsernameLinkRateLimited() {
|
||||||
|
final Duration retryAfter = Duration.ofSeconds(17);
|
||||||
|
|
||||||
|
when(rateLimiter.validateReactive(anyString()))
|
||||||
|
.thenReturn(Mono.error(new RateLimitExceededException(retryAfter, false)));
|
||||||
|
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
|
||||||
|
() -> unauthenticatedServiceStub().lookupUsernameLink(LookupUsernameLinkRequest.newBuilder()
|
||||||
|
.setUsernameLinkHandle(UUIDUtil.toByteString(UUID.randomUUID()))
|
||||||
|
.build()),
|
||||||
|
accountsManager);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue