From b1fd025ea67b06753f1a309a00679e9d00155b30 Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Wed, 29 Nov 2023 14:52:40 -0600 Subject: [PATCH] Use EpochSecondSerializer for UserRemoteConfigList.serverEpochTime --- .../textsecuregcm/WhisperServerService.java | 3 ++- .../controllers/RemoteConfigController.java | 9 +++++-- .../entities/UserRemoteConfigList.java | 7 ++--- .../textsecuregcm/util/InstantAdapter.java | 26 +++++++++++++++++++ .../RemoteConfigControllerTest.java | 24 ++++++++++++++++- 5 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/util/InstantAdapter.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index d1e3e3cf8..64a011fb4 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -822,7 +822,8 @@ public class WhisperServerService extends Application configAuthUsers, String requiredHostedDomain, List audience, - final GoogleIdTokenVerifier.Builder googleIdTokenVerifierBuilder, Map globalConfig) { + final GoogleIdTokenVerifier.Builder googleIdTokenVerifierBuilder, Map globalConfig, + final Clock clock) { this.remoteConfigsManager = remoteConfigsManager; this.adminEventLogger = Objects.requireNonNull(adminEventLogger); this.configAuthUsers = configAuthUsers; @@ -73,6 +76,8 @@ public class RemoteConfigController { this.requiredHostedDomain = requiredHostedDomain; this.googleIdTokenVerifier = googleIdTokenVerifierBuilder.setAudience(audience).build(); + + this.clock = clock; } @GET @@ -90,7 +95,7 @@ public class RemoteConfigController { config.getUuids()); return new UserRemoteConfig(config.getName(), inBucket, inBucket ? config.getValue() : config.getDefaultValue()); - }), globalConfigStream).collect(Collectors.toList()), Clock.systemUTC().instant()); + }), globalConfigStream).collect(Collectors.toList()), clock.instant()); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfigList.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfigList.java index de10314f1..e913b5e62 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfigList.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/UserRemoteConfigList.java @@ -7,10 +7,10 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; - +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.List; +import org.whispersystems.textsecuregcm.util.InstantAdapter; public class UserRemoteConfigList { @@ -18,6 +18,7 @@ public class UserRemoteConfigList { private List config; @JsonProperty + @JsonSerialize(using = InstantAdapter.EpochSecondSerializer.class) @JsonFormat(shape = JsonFormat.Shape.NUMBER_INT) private Instant serverEpochTime; @@ -25,7 +26,7 @@ public class UserRemoteConfigList { public UserRemoteConfigList(List config, Instant serverEpochTime) { this.config = config; - this.serverEpochTime = serverEpochTime != null ? serverEpochTime.truncatedTo(ChronoUnit.SECONDS) : null; + this.serverEpochTime = serverEpochTime; } public List getConfig() { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/InstantAdapter.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/InstantAdapter.java new file mode 100644 index 000000000..14355893e --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/InstantAdapter.java @@ -0,0 +1,26 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.time.Instant; + +public class InstantAdapter { + + public static class EpochSecondSerializer extends JsonSerializer { + + @Override + public void serialize(final Instant value, final JsonGenerator gen, final SerializerProvider serializers) + throws IOException { + + gen.writeNumber(value.getEpochSecond()); + } + } + +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java index 48ea3db70..a24ab4531 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RemoteConfigControllerTest.java @@ -23,6 +23,7 @@ import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -34,6 +35,8 @@ import java.util.Set; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.InstanceOfAssertFactory; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -49,6 +52,7 @@ import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapp import org.whispersystems.textsecuregcm.storage.RemoteConfig; import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.TestClock; @ExtendWith(DropwizardExtensionsSupport.class) class RemoteConfigControllerTest { @@ -67,6 +71,10 @@ class RemoteConfigControllerTest { when(googleIdVerificationTokenBuilder.build()).thenReturn(googleIdTokenVerifier); } + private static final long PINNED_EPOCH_SECONDS = 1701287216L; + private static final TestClock TEST_CLOCK = TestClock.pinned(Instant.ofEpochSecond(PINNED_EPOCH_SECONDS)); + + private static final ResourceExtension resources = ResourceExtension.builder() .addProvider(AuthHelper.getAuthFilter()) .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( @@ -75,7 +83,7 @@ class RemoteConfigControllerTest { .addProvider(new DeviceLimitExceededExceptionMapper()) .addResource(new RemoteConfigController(remoteConfigsManager, new NoOpAdminEventLogger(), remoteConfigsUsers, requiredHostedDomain, Collections.singletonList("aud.example.com"), - googleIdVerificationTokenBuilder, Map.of("maxGroupSize", "42"))) + googleIdVerificationTokenBuilder, Map.of("maxGroupSize", "42"), TEST_CLOCK)) .build(); @@ -153,6 +161,20 @@ class RemoteConfigControllerTest { assertThat(configuration.getConfig().get(10).getName()).isEqualTo("global.maxGroupSize"); } + @Test + void testServerEpochTime() { + Object serverEpochTime = resources.getJerseyTest() + .target("/v1/config/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(Map.class) + .get("serverEpochTime"); + + assertThat(serverEpochTime).asInstanceOf(new InstanceOfAssertFactory<>(Number.class, Assertions::assertThat)) + .extracting(Number::longValue) + .isEqualTo(PINNED_EPOCH_SECONDS); + } + @Test void testRetrieveConfigNotSpecial() { UserRemoteConfigList configuration = resources.getJerseyTest()