Skip to content

Commit 46b8967

Browse files
authored
Merge pull request #502 from IABTechLab/gdm-UID2-5601-salt-rotate-limit-per-age
Added max rotatable salts per age limit
2 parents cc74967 + 91479f6 commit 46b8967

File tree

3 files changed

+75
-21
lines changed

3 files changed

+75
-21
lines changed

src/main/java/com/uid2/admin/salt/SaltRotation.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
public class SaltRotation {
1919
private static final long THIRTY_DAYS_IN_MS = Duration.ofDays(30).toMillis();
20+
private static final double MAX_SALT_PERCENTAGE = 0.8;
2021

2122
private final IKeyGenerator keyGenerator;
2223
private final boolean isRefreshFromEnabled;
@@ -133,6 +134,8 @@ private List<SaltEntry> pickSaltsToRotate(
133134
TargetDate targetDate,
134135
Duration[] minAges,
135136
int numSaltsToRotate) {
137+
var maxSaltsPerAge = this.isRefreshFromEnabled ? (int) (numSaltsToRotate * MAX_SALT_PERCENTAGE) : numSaltsToRotate;
138+
136139
var thresholds = Arrays.stream(minAges)
137140
.map(minAge -> targetDate.asInstant().minusSeconds(minAge.getSeconds()))
138141
.sorted()
@@ -146,7 +149,7 @@ private List<SaltEntry> pickSaltsToRotate(
146149
var maxIndexes = numSaltsToRotate - indexesToRotate.size();
147150
var saltsToRotate = pickSaltsToRotateInTimeWindow(
148151
refreshableSalts,
149-
maxIndexes,
152+
Math.min(maxIndexes, maxSaltsPerAge),
150153
minLastUpdated.toEpochMilli(),
151154
maxLastUpdated.toEpochMilli()
152155
);

src/test/java/com/uid2/admin/salt/SaltRotationTest.java

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ void teardown() throws Exception {
5656
}
5757

5858
@Test
59-
void rotateSaltsLastSnapshotIsUpToDate() throws Exception {
59+
void testRotateSaltsLastSnapshotIsUpToDate() throws Exception {
6060
final Duration[] minAges = {
6161
Duration.ofDays(1),
6262
Duration.ofDays(2),
@@ -74,7 +74,7 @@ void rotateSaltsLastSnapshotIsUpToDate() throws Exception {
7474
}
7575

7676
@Test
77-
void rotateSaltsAllSaltsUpToDate() throws Exception {
77+
void testRotateSaltsAllSaltsUpToDate() throws Exception {
7878
final Duration[] minAges = {
7979
Duration.ofDays(1),
8080
Duration.ofDays(2),
@@ -90,7 +90,7 @@ void rotateSaltsAllSaltsUpToDate() throws Exception {
9090
}
9191

9292
@Test
93-
void rotateSaltsAllSaltsOld() throws Exception {
93+
void testRotateSaltsAllSaltsOld() throws Exception {
9494
final Duration[] minAges = {
9595
Duration.ofDays(1),
9696
Duration.ofDays(2),
@@ -110,7 +110,7 @@ void rotateSaltsAllSaltsOld() throws Exception {
110110
}
111111

112112
@Test
113-
void rotateSaltsRotateSaltsFromOldestBucketOnly() throws Exception {
113+
void testRotateSaltsRotateSaltsFromOldestBucketOnly() throws Exception {
114114
final Duration[] minAges = {
115115
Duration.ofDays(5),
116116
Duration.ofDays(4),
@@ -135,7 +135,7 @@ void rotateSaltsRotateSaltsFromOldestBucketOnly() throws Exception {
135135
}
136136

137137
@Test
138-
void rotateSaltsRotateSaltsFromNewerBucketOnly() throws Exception {
138+
void testRotateSaltsRotateSaltsFromNewerBucketOnly() throws Exception {
139139
final Duration[] minAges = {
140140
Duration.ofDays(5),
141141
Duration.ofDays(3),
@@ -158,7 +158,7 @@ void rotateSaltsRotateSaltsFromNewerBucketOnly() throws Exception {
158158
}
159159

160160
@Test
161-
void rotateSaltsRotateSaltsFromMultipleBuckets() throws Exception {
161+
void testRotateSaltsRotateSaltsFromMultipleBuckets() throws Exception {
162162
final Duration[] minAges = {
163163
Duration.ofDays(5),
164164
Duration.ofDays(4),
@@ -183,7 +183,7 @@ void rotateSaltsRotateSaltsFromMultipleBuckets() throws Exception {
183183
}
184184

185185
@Test
186-
void rotateSaltsRotateSaltsInsufficientOutdatedSalts() throws Exception {
186+
void testRotateSaltsRotateSaltsInsufficientOutdatedSalts() throws Exception {
187187
final Duration[] minAges = {
188188
Duration.ofDays(5),
189189
Duration.ofDays(3),
@@ -232,7 +232,7 @@ void testRefreshFromCalculation(int lastRotationDaysAgo, int lastRotationMsOffse
232232
}
233233

234234
@Test
235-
void rotateSaltsPopulatePreviousSaltsOnRotation() throws Exception {
235+
void testRotateSaltsPopulatePreviousSaltsOnRotation() throws Exception {
236236
final Duration[] minAges = {
237237
Duration.ofDays(90),
238238
Duration.ofDays(60),
@@ -260,7 +260,7 @@ void rotateSaltsPopulatePreviousSaltsOnRotation() throws Exception {
260260
}
261261

262262
@Test
263-
void rotateSaltsPreservePreviousSaltsLessThan90DaysOld() throws Exception {
263+
void testRotateSaltsPreservePreviousSaltsLessThan90DaysOld() throws Exception {
264264
final Duration[] minAges = {
265265
Duration.ofDays(60),
266266
};
@@ -285,7 +285,7 @@ void rotateSaltsPreservePreviousSaltsLessThan90DaysOld() throws Exception {
285285
}
286286

287287
@Test
288-
void rotateSaltsRemovePreviousSaltsOver90DaysOld() throws Exception {
288+
void testRotateSaltsRemovePreviousSaltsOver90DaysOld() throws Exception {
289289
final Duration[] minAges = {
290290
Duration.ofDays(100),
291291
};
@@ -309,9 +309,8 @@ void rotateSaltsRemovePreviousSaltsOver90DaysOld() throws Exception {
309309
assertNull(salts[1].previousSalt());
310310
}
311311

312-
313312
@Test
314-
void rotateSaltsRotateWhenRefreshFromIsTargetDate() throws Exception {
313+
void testRotateSaltsRotateWhenRefreshFromIsTargetDate() throws Exception {
315314
JsonObject config = new JsonObject();
316315
config.put(AdminConst.ENABLE_SALT_ROTATION_REFRESH_FROM, Boolean.TRUE);
317316
saltRotation = new SaltRotation(config, keyGenerator);
@@ -351,35 +350,40 @@ void rotateSaltsRotateWhenRefreshFromIsTargetDate() throws Exception {
351350
}
352351

353352
@Test
354-
void logsSaltAgesOnRotation() throws Exception {
353+
void testLogFewSaltAgesOnRotation() throws Exception {
355354
JsonObject config = new JsonObject();
356355
config.put(AdminConst.ENABLE_SALT_ROTATION_REFRESH_FROM, Boolean.TRUE);
357356
saltRotation = new SaltRotation(config, keyGenerator);
358357

358+
// 7 salts total, 5 refreshable, 3 will rotate (6 * 0.4 rounded up), up to 2 will rotate per age (3 * 0.8)
359359
var lastSnapshot = SaltSnapshotBuilder.start()
360360
.entries(
361-
// 5 salts total, 3 refreshable, 2 rotated given 40% fraction
362-
SaltBuilder.start().lastUpdated(daysEarlier(65)).refreshFrom(targetDate()), // Refreshable, old enough, rotated
361+
SaltBuilder.start().lastUpdated(daysEarlier(65)).refreshFrom(targetDate()), // Refreshable, old enough
363362
SaltBuilder.start().lastUpdated(daysEarlier(5)).refreshFrom(targetDate()), // Refreshable, too new
363+
SaltBuilder.start().lastUpdated(daysEarlier(33)).refreshFrom(targetDate()), // Refreshable, old enough
364364
SaltBuilder.start().lastUpdated(daysEarlier(50)).refreshFrom(daysLater(1)), // Not refreshable, old enough
365-
SaltBuilder.start().lastUpdated(daysEarlier(65)).refreshFrom(targetDate()), // Refreshable, old enough, rotated
365+
SaltBuilder.start().lastUpdated(daysEarlier(65)).refreshFrom(targetDate()), // Refreshable, old enough
366+
SaltBuilder.start().lastUpdated(daysEarlier(65)).refreshFrom(targetDate()), // Refreshable, old enough
366367
SaltBuilder.start().lastUpdated(daysEarlier(10)).refreshFrom(daysLater(10)) // Not refreshable, too new
367368
)
368369
.build();
369370

370371
var expected = Set.of(
371372
"[INFO] Salt rotation complete target_date=2025-01-01",
372373
// Post-rotation ages, we want to look at current state
373-
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=0 salt_count=2", // The two rotated salts, used to be 65 and 50 days old
374+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=0 salt_count=3",
374375
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=5 salt_count=1",
375376
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=10 salt_count=1",
376377
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=50 salt_count=1",
378+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=65 salt_count=1",
377379

378380
// Pre-rotation ages, we want to see at which ages salts become refreshable, post rotation some will be 0
379381
"[INFO] salt_count_type=refreshable-salts target_date=2025-01-01 age=5 salt_count=1",
380-
"[INFO] salt_count_type=refreshable-salts target_date=2025-01-01 age=65 salt_count=2",
382+
"[INFO] salt_count_type=refreshable-salts target_date=2025-01-01 age=33 salt_count=1",
383+
"[INFO] salt_count_type=refreshable-salts target_date=2025-01-01 age=65 salt_count=3",
381384

382385
// Pre-rotation ages, post rotation they will all have age 0
386+
"[INFO] salt_count_type=rotated-salts target_date=2025-01-01 age=33 salt_count=1",
383387
"[INFO] salt_count_type=rotated-salts target_date=2025-01-01 age=65 salt_count=2"
384388
);
385389

@@ -390,12 +394,53 @@ void logsSaltAgesOnRotation() throws Exception {
390394
assertThat(actual).isEqualTo(expected);
391395
}
392396

397+
@Test
398+
void testLogManySaltAgesOnRotation() throws Exception {
399+
JsonObject config = new JsonObject();
400+
config.put(AdminConst.ENABLE_SALT_ROTATION_REFRESH_FROM, Boolean.TRUE);
401+
saltRotation = new SaltRotation(config, keyGenerator);
402+
403+
// 50 salts total, 16 refreshable, 10 will rotate (18 * 0.2 rounded up), up to 8 will rotate per age (10 * 0.8)
404+
var lastSnapshot = SaltSnapshotBuilder.start()
405+
.entries(10, daysEarlier(5), targetDate()) // Refreshable, too new
406+
.entries(10, daysEarlier(10), daysLater(10)) // Not refreshable, too new
407+
.entries(10, daysEarlier(33), targetDate()) // Refreshable, old enough
408+
.entries(10, daysEarlier(50), daysLater(1)) // Not refreshable, old enough
409+
.entries(10, daysEarlier(65), targetDate()) // Refreshable, old enough
410+
.build();
411+
412+
var expected = Set.of(
413+
"[INFO] Salt rotation complete target_date=2025-01-01",
414+
// Post-rotation ages, we want to look at current state
415+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=0 salt_count=10",
416+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=5 salt_count=10",
417+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=10 salt_count=10",
418+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=33 salt_count=8",
419+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=50 salt_count=10",
420+
"[INFO] salt_count_type=total-salts target_date=2025-01-01 age=65 salt_count=2",
421+
422+
// Pre-rotation ages, we want to see at which ages salts become refreshable, post rotation some will be 0
423+
"[INFO] salt_count_type=refreshable-salts target_date=2025-01-01 age=5 salt_count=10",
424+
"[INFO] salt_count_type=refreshable-salts target_date=2025-01-01 age=33 salt_count=10",
425+
"[INFO] salt_count_type=refreshable-salts target_date=2025-01-01 age=65 salt_count=10",
426+
427+
// Pre-rotation ages, post rotation they will all have age 0
428+
"[INFO] salt_count_type=rotated-salts target_date=2025-01-01 age=33 salt_count=2",
429+
"[INFO] salt_count_type=rotated-salts target_date=2025-01-01 age=65 salt_count=8"
430+
);
431+
432+
var minAges = new Duration[]{Duration.ofDays(30), Duration.ofDays(60)};
433+
saltRotation.rotateSalts(lastSnapshot, minAges, 0.2, targetDate());
434+
435+
var actual = appender.list.stream().map(Object::toString).collect(Collectors.toSet());
436+
assertThat(actual).isEqualTo(expected);
437+
}
438+
393439
private int countEntriesWithLastUpdated(SaltEntry[] entries, TargetDate lastUpdated) {
394440
return countEntriesWithLastUpdated(entries, lastUpdated.asInstant());
395441
}
396442

397443
private int countEntriesWithLastUpdated(SaltEntry[] entries, Instant lastUpdated) {
398444
return (int) Arrays.stream(entries).filter(e -> e.lastUpdated() == lastUpdated.toEpochMilli()).count();
399445
}
400-
401446
}

src/test/java/com/uid2/admin/salt/helper/SaltSnapshotBuilder.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.uid2.admin.salt.helper;
22

3-
import com.uid2.admin.salt.SaltRotation;
43
import com.uid2.admin.salt.TargetDate;
54
import com.uid2.shared.model.SaltEntry;
65
import com.uid2.shared.store.salt.RotatingSaltProvider;
@@ -33,6 +32,13 @@ public SaltSnapshotBuilder entries(int count, TargetDate lastUpdated) {
3332
return this;
3433
}
3534

35+
public SaltSnapshotBuilder entries(int count, TargetDate lastUpdated, TargetDate refreshFrom) {
36+
for (int i = 0; i < count; ++i) {
37+
entries.add(SaltBuilder.start().lastUpdated(lastUpdated).refreshFrom(refreshFrom).build());
38+
}
39+
return this;
40+
}
41+
3642
public SaltSnapshotBuilder entries(SaltBuilder... salts) {
3743
SaltEntry[] builtSalts = Arrays.stream(salts).map(SaltBuilder::build).toArray(SaltEntry[]::new);
3844
Collections.addAll(this.entries, builtSalts);

0 commit comments

Comments
 (0)