diff --git a/pom.xml b/pom.xml index 014aed45..59fc0859 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.uid2 uid2-admin - 6.9.0 + 6.9.1-alpha-202-SNAPSHOT UTF-8 diff --git a/src/main/java/com/uid2/admin/AdminConst.java b/src/main/java/com/uid2/admin/AdminConst.java index c4296d3e..c507cd58 100644 --- a/src/main/java/com/uid2/admin/AdminConst.java +++ b/src/main/java/com/uid2/admin/AdminConst.java @@ -6,4 +6,5 @@ public class AdminConst { public static final String ROLE_OKTA_GROUP_MAP_PRIVILEGED = "role_okta_group_map_privileged"; public static final String ROLE_OKTA_GROUP_MAP_SUPER_USER = "role_okta_group_map_super_user"; public static final String ENABLE_SALT_ROTATION_REFRESH_FROM = "enable_salt_rotation_refresh_from"; + public static final String ENABLE_SALT_ROTATION_CUSTOM_AGE_THRESHOLDS = "enable_salt_rotation_custom_age_thresholds"; } diff --git a/src/main/java/com/uid2/admin/salt/SaltRotation.java b/src/main/java/com/uid2/admin/salt/SaltRotation.java index adcdf151..cd2887d0 100644 --- a/src/main/java/com/uid2/admin/salt/SaltRotation.java +++ b/src/main/java/com/uid2/admin/salt/SaltRotation.java @@ -23,11 +23,17 @@ public class SaltRotation { private final IKeyGenerator keyGenerator; private final boolean isRefreshFromEnabled; + private final boolean isCustomAgeThresholdEnabled; private static final Logger LOGGER = LoggerFactory.getLogger(SaltRotation.class); public SaltRotation(JsonObject config, IKeyGenerator keyGenerator) { this.keyGenerator = keyGenerator; this.isRefreshFromEnabled = config.getBoolean(AdminConst.ENABLE_SALT_ROTATION_REFRESH_FROM, false); + this.isCustomAgeThresholdEnabled = config.getBoolean(AdminConst.ENABLE_SALT_ROTATION_CUSTOM_AGE_THRESHOLDS, false); + } + + public boolean isCustomAgeThresholdEnabled() { + return this.isCustomAgeThresholdEnabled; } public Result rotateSalts( diff --git a/src/main/java/com/uid2/admin/vertx/service/SaltService.java b/src/main/java/com/uid2/admin/vertx/service/SaltService.java index fe9c76f7..05862cda 100644 --- a/src/main/java/com/uid2/admin/vertx/service/SaltService.java +++ b/src/main/java/com/uid2/admin/vertx/service/SaltService.java @@ -21,15 +21,28 @@ import java.time.*; import java.time.format.DateTimeFormatter; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import static com.uid2.admin.vertx.Endpoints.*; public class SaltService implements IService { private static final Logger LOGGER = LoggerFactory.getLogger(SaltService.class); + private static final Duration[] SALT_ROTATION_AGE_THRESHOLDS = new Duration[]{ + Duration.ofDays(30), + Duration.ofDays(60), + Duration.ofDays(90), + Duration.ofDays(120), + Duration.ofDays(150), + Duration.ofDays(180), + Duration.ofDays(210), + Duration.ofDays(240), + Duration.ofDays(270), + Duration.ofDays(300), + Duration.ofDays(330), + Duration.ofDays(360), + Duration.ofDays(390) + }; private final AdminAuthMiddleware auth; private final WriteLock writeLock; @@ -37,6 +50,7 @@ public class SaltService implements IService { private final RotatingSaltProvider saltProvider; private final SaltRotation saltRotation; + public SaltService(AdminAuthMiddleware auth, WriteLock writeLock, SaltStoreWriter storageManager, @@ -117,8 +131,15 @@ private void handleSaltRotate(RoutingContext rc) { try { final Optional fraction = RequestUtil.getDouble(rc, "fraction"); if (fraction.isEmpty()) return; - final Duration[] minAges = RequestUtil.getDurations(rc, "min_ages_in_seconds"); - if (minAges == null) return; + + final Duration[] ageThresholds; + if (saltRotation.isCustomAgeThresholdEnabled()) { + ageThresholds = RequestUtil.getDurations(rc, "min_ages_in_seconds"); + if (ageThresholds == null) return; + } else { + ageThresholds = SALT_ROTATION_AGE_THRESHOLDS; + } + LOGGER.info("Salt rotation age thresholds in seconds: {}", Arrays.stream(ageThresholds).map(Duration::toSeconds).collect(Collectors.toList())); final TargetDate targetDate = RequestUtil.getDate(rc, "target_date", DateTimeFormatter.ISO_LOCAL_DATE) @@ -134,7 +155,7 @@ private void handleSaltRotate(RoutingContext rc) { final List snapshots = saltProvider.getSnapshots(); final RotatingSaltProvider.SaltSnapshot lastSnapshot = snapshots.getLast(); - final SaltRotation.Result result = saltRotation.rotateSalts(lastSnapshot, minAges, fraction.get(), targetDate); + final SaltRotation.Result result = saltRotation.rotateSalts(lastSnapshot, ageThresholds, fraction.get(), targetDate); if (!result.hasSnapshot()) { ResponseUtil.error(rc, 200, result.getReason()); return; diff --git a/src/test/java/com/uid2/admin/salt/SaltServiceTest.java b/src/test/java/com/uid2/admin/salt/SaltServiceTest.java index 7d4755f0..508d3ab2 100644 --- a/src/test/java/com/uid2/admin/salt/SaltServiceTest.java +++ b/src/test/java/com/uid2/admin/salt/SaltServiceTest.java @@ -11,7 +11,7 @@ import io.vertx.junit5.VertxTestContext; import org.junit.jupiter.api.Test; import org.mockito.Mock; - +import java.time.Duration; import java.time.Instant; import java.util.Arrays; @@ -111,7 +111,7 @@ void rotateSaltsNoNewSnapshot(Vertx vertx, VertxTestContext testContext) throws } @Test - void rotateSaltsWitnSpecificTargetDate(Vertx vertx, VertxTestContext testContext) throws Exception { + void rotateSaltsWithSpecificTargetDate(Vertx vertx, VertxTestContext testContext) throws Exception { fakeAuth(Role.SUPER_USER); final SaltSnapshotBuilder[] snapshots = { SaltSnapshotBuilder.start().effective(daysEarlier(5)).expires(daysEarlier(4)).entries(10, daysEarlier(5)), @@ -133,6 +133,75 @@ void rotateSaltsWitnSpecificTargetDate(Vertx vertx, VertxTestContext testContext }); } + @Test + void rotateSaltsWithCustomAgeThresholdsEnabled(Vertx vertx, VertxTestContext testContext) throws Exception { + fakeAuth(Role.SUPER_USER); + + when(saltRotation.isCustomAgeThresholdEnabled()).thenReturn(true); + + final SaltSnapshotBuilder lastSnapshot = SaltSnapshotBuilder.start().effective(daysEarlier(1)).expires(daysLater(6)).entries(1, daysEarlier(1)); + setSnapshots(lastSnapshot); + + var result = SaltRotation.Result.fromSnapshot(SaltSnapshotBuilder.start().effective(targetDate()).expires(daysEarlier(7)).entries(1, targetDate()).build()); + + Duration[] expectedCustomAgeThresholds = new Duration[]{ + Duration.ofSeconds(50), + Duration.ofSeconds(60), + Duration.ofSeconds(70) + }; + + when(saltRotation.rotateSalts(any(), eq(expectedCustomAgeThresholds), eq(0.2), eq(utcTomorrow))).thenReturn(result); + + post(vertx, testContext, "api/salt/rotate?min_ages_in_seconds=50,60,70&fraction=0.2", "", response -> { + verify(saltRotation).rotateSalts(any(), eq(expectedCustomAgeThresholds), eq(0.2), eq(utcTomorrow)); + assertEquals(200, response.statusCode()); + testContext.completeNow(); + }); + } + + @Test + void rotateSaltsWithDefaultAgeThresholds(Vertx vertx, VertxTestContext testContext) throws Exception { + fakeAuth(Role.SUPER_USER); + + when(saltRotation.isCustomAgeThresholdEnabled()).thenReturn(false); + + final SaltSnapshotBuilder lastSnapshot = SaltSnapshotBuilder.start().effective(daysEarlier(1)).expires(daysLater(6)).entries(1, daysEarlier(1)); + setSnapshots(lastSnapshot); + + var result = SaltRotation.Result.fromSnapshot(SaltSnapshotBuilder.start().effective(targetDate()).expires(daysEarlier(7)).entries(1, targetDate()).build()); + + Duration[] expectedDefaultAgeThresholds = new Duration[]{ + Duration.ofDays(30), Duration.ofDays(60), Duration.ofDays(90), Duration.ofDays(120), + Duration.ofDays(150), Duration.ofDays(180), Duration.ofDays(210), Duration.ofDays(240), + Duration.ofDays(270), Duration.ofDays(300), Duration.ofDays(330), Duration.ofDays(360), + Duration.ofDays(390) + }; + + when(saltRotation.rotateSalts(any(), eq(expectedDefaultAgeThresholds), eq(0.2), eq(utcTomorrow))).thenReturn(result); + + post(vertx, testContext, "api/salt/rotate?min_ages_in_seconds=50,60,70&fraction=0.2", "", response -> { + verify(saltRotation).rotateSalts(any(), eq(expectedDefaultAgeThresholds), eq(0.2), eq(utcTomorrow)); + assertEquals(200, response.statusCode()); + testContext.completeNow(); + }); + } + + @Test + void rotateSaltsWithCustomAgeThresholdsEnabledButMissingParameter(Vertx vertx, VertxTestContext testContext) { + fakeAuth(Role.SUPER_USER); + + when(saltRotation.isCustomAgeThresholdEnabled()).thenReturn(true); + + final SaltSnapshotBuilder lastSnapshot = SaltSnapshotBuilder.start().effective(daysEarlier(1)).expires(daysLater(6)).entries(1, daysEarlier(1)); + setSnapshots(lastSnapshot); + + post(vertx, testContext, "api/salt/rotate?fraction=0.2", "", response -> { + verify(saltRotation, never()).rotateSalts(any(), any(), anyDouble(), any()); + assertEquals(400, response.statusCode()); + testContext.completeNow(); + }); + } + private void checkSnapshotsResponse(SaltSnapshotBuilder[] expectedSnapshots, Object[] actualSnapshots) { assertEquals(expectedSnapshots.length, actualSnapshots.length); for (int i = 0; i < expectedSnapshots.length; ++i) {