Use hashed UUID to spread last seen updates over a full day (#40)
This commit is contained in:
parent
1e7b6f78ca
commit
eede4e50ca
|
@ -3,6 +3,8 @@ package org.whispersystems.textsecuregcm.auth;
|
||||||
import com.codahale.metrics.Meter;
|
import com.codahale.metrics.Meter;
|
||||||
import com.codahale.metrics.MetricRegistry;
|
import com.codahale.metrics.MetricRegistry;
|
||||||
import com.codahale.metrics.SharedMetricRegistries;
|
import com.codahale.metrics.SharedMetricRegistries;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.dropwizard.auth.basic.BasicCredentials;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.textsecuregcm.storage.Account;
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
@ -11,11 +13,12 @@ import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
import org.whispersystems.textsecuregcm.util.Constants;
|
import org.whispersystems.textsecuregcm.util.Constants;
|
||||||
import org.whispersystems.textsecuregcm.util.Util;
|
import org.whispersystems.textsecuregcm.util.Util;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import static com.codahale.metrics.MetricRegistry.name;
|
import static com.codahale.metrics.MetricRegistry.name;
|
||||||
import io.dropwizard.auth.basic.BasicCredentials;
|
|
||||||
|
|
||||||
public class BaseAccountAuthenticator {
|
public class BaseAccountAuthenticator {
|
||||||
|
|
||||||
|
@ -31,9 +34,16 @@ public class BaseAccountAuthenticator {
|
||||||
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
|
private final Logger logger = LoggerFactory.getLogger(AccountAuthenticator.class);
|
||||||
|
|
||||||
private final AccountsManager accountsManager;
|
private final AccountsManager accountsManager;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
public BaseAccountAuthenticator(AccountsManager accountsManager) {
|
public BaseAccountAuthenticator(AccountsManager accountsManager) {
|
||||||
|
this(accountsManager, Clock.systemUTC());
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public BaseAccountAuthenticator(AccountsManager accountsManager, Clock clock) {
|
||||||
this.accountsManager = accountsManager;
|
this.accountsManager = accountsManager;
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
|
public Optional<Account> authenticate(BasicCredentials basicCredentials, boolean enabledRequired) {
|
||||||
|
@ -80,9 +90,13 @@ public class BaseAccountAuthenticator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateLastSeen(Account account, Device device) {
|
@VisibleForTesting
|
||||||
if (device.getLastSeen() != Util.todayInMillis()) {
|
public void updateLastSeen(Account account, Device device) {
|
||||||
device.setLastSeen(Util.todayInMillis());
|
final long lastSeenOffsetSeconds = Math.abs(account.getUuid().getLeastSignificantBits()) % ChronoUnit.DAYS.getDuration().toSeconds();
|
||||||
|
final long todayInMillisWithOffset = Util.todayInMillisGivenOffsetFromNow(clock, Duration.ofSeconds(lastSeenOffsetSeconds).negated());
|
||||||
|
|
||||||
|
if (device.getLastSeen() < todayInMillisWithOffset) {
|
||||||
|
device.setLastSeen(Util.todayInMillis(clock));
|
||||||
accountsManager.update(account);
|
accountsManager.update(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ import java.net.URLEncoder;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.temporal.ChronoField;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -186,6 +189,15 @@ public class Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long todayInMillis() {
|
public static long todayInMillis() {
|
||||||
return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
|
return todayInMillis(Clock.systemUTC());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long todayInMillis(Clock clock) {
|
||||||
|
return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(clock.instant().toEpochMilli()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long todayInMillisGivenOffsetFromNow(Clock clock, Duration offset) {
|
||||||
|
final long currentTimeSeconds = offset.addTo(clock.instant()).getLong(ChronoField.INSTANT_SECONDS);
|
||||||
|
return TimeUnit.DAYS.toMillis(TimeUnit.SECONDS.toDays(currentTimeSeconds));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
package org.whispersystems.textsecuregcm.tests.auth;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.whispersystems.textsecuregcm.auth.BaseAccountAuthenticator;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class BaseAccountAuthenticatorTest {
|
||||||
|
|
||||||
|
private final Random random = new Random(867_5309L);
|
||||||
|
private final long today = 1590451200000L;
|
||||||
|
private final long yesterday = today - 86_400_000L;
|
||||||
|
private final long oldTime = yesterday - 86_400_000L;
|
||||||
|
private final long currentTime = today + 68_000_000L;
|
||||||
|
|
||||||
|
private AccountsManager accountsManager;
|
||||||
|
private BaseAccountAuthenticator baseAccountAuthenticator;
|
||||||
|
private Clock clock;
|
||||||
|
private Account acct1;
|
||||||
|
private Account acct2;
|
||||||
|
private Account oldAccount;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
accountsManager = mock(AccountsManager.class);
|
||||||
|
clock = mock(Clock.class);
|
||||||
|
baseAccountAuthenticator = new BaseAccountAuthenticator(accountsManager, clock);
|
||||||
|
|
||||||
|
acct1 = new Account("+14088675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, null, null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
|
||||||
|
acct2 = new Account("+14098675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, null, null, null, null, false, 0, null, yesterday, 0, null, 0, null)), null);
|
||||||
|
oldAccount = new Account("+14108675309", AuthHelper.getRandomUUID(random), Set.of(new Device(1, null, null, null, null, null, null, null, false, 0, null, oldTime, 0, null, 0, null)), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateLastSeenMiddleOfDay() {
|
||||||
|
when(clock.instant()).thenReturn(Instant.ofEpochMilli(currentTime));
|
||||||
|
|
||||||
|
baseAccountAuthenticator.updateLastSeen(acct1, acct1.getDevices().stream().findFirst().get());
|
||||||
|
baseAccountAuthenticator.updateLastSeen(acct2, acct2.getDevices().stream().findFirst().get());
|
||||||
|
|
||||||
|
verify(accountsManager, never()).update(acct1);
|
||||||
|
verify(accountsManager).update(acct2);
|
||||||
|
|
||||||
|
assertThat(acct1.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(yesterday);
|
||||||
|
assertThat(acct2.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateLastSeenStartOfDay() {
|
||||||
|
when(clock.instant()).thenReturn(Instant.ofEpochMilli(today));
|
||||||
|
|
||||||
|
baseAccountAuthenticator.updateLastSeen(acct1, acct1.getDevices().stream().findFirst().get());
|
||||||
|
baseAccountAuthenticator.updateLastSeen(acct2, acct2.getDevices().stream().findFirst().get());
|
||||||
|
|
||||||
|
verify(accountsManager, never()).update(acct1);
|
||||||
|
verify(accountsManager, never()).update(acct2);
|
||||||
|
|
||||||
|
assertThat(acct1.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(yesterday);
|
||||||
|
assertThat(acct2.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(yesterday);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUpdateLastSeenEndOfDay() {
|
||||||
|
when(clock.instant()).thenReturn(Instant.ofEpochMilli(today + 86_400_000L - 1));
|
||||||
|
|
||||||
|
baseAccountAuthenticator.updateLastSeen(acct1, acct1.getDevices().stream().findFirst().get());
|
||||||
|
baseAccountAuthenticator.updateLastSeen(acct2, acct2.getDevices().stream().findFirst().get());
|
||||||
|
|
||||||
|
verify(accountsManager).update(acct1);
|
||||||
|
verify(accountsManager).update(acct2);
|
||||||
|
|
||||||
|
assertThat(acct1.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today);
|
||||||
|
assertThat(acct2.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNeverWriteYesterday() {
|
||||||
|
when(clock.instant()).thenReturn(Instant.ofEpochMilli(today));
|
||||||
|
|
||||||
|
baseAccountAuthenticator.updateLastSeen(oldAccount, oldAccount.getDevices().stream().findFirst().get());
|
||||||
|
|
||||||
|
verify(accountsManager).update(oldAccount);
|
||||||
|
|
||||||
|
assertThat(oldAccount.getDevices().stream().findFirst().get().getLastSeen()).isEqualTo(today);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue