Skip to content

Commit 4546802

Browse files
committed
Validate the token state only in the transformer when regenerating a secret
Motivation: The regeneration endpoint passed a snapshot of the authorized token into the content transformer and compared it against the current state. The snapshot is taken outside the commit lock, so the comparisons were a non-atomic double check of what the transformer already validates atomically. Modifications: - Remove the expected-token parameter and its comparisons; the token state is validated only in the content transformer. Result: - Validation happens once at the atomic point. Concurrent regenerations follow last-writer-wins semantics.
1 parent 8c8f588 commit 4546802

4 files changed

Lines changed: 1 addition & 88 deletions

File tree

server/src/main/java/com/linecorp/centraldogma/server/internal/api/sysadmin/AppIdentityRegistryService.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,7 @@ public CompletableFuture<Token> regenerateTokenSecret(ServiceRequestContext ctx,
270270
throw new IllegalArgumentException(
271271
"You can't regenerate the secret of an active token. Deactivate it first.");
272272
}
273-
// Pass the authorized token so that the regeneration fails if the token is
274-
// recreated or regenerated concurrently in the meantime.
275-
return mds.regenerateTokenSecret(author, appId, token);
273+
return mds.regenerateTokenSecret(author, appId);
276274
});
277275
}
278276

server/src/main/java/com/linecorp/centraldogma/server/metadata/AppIdentityService.java

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,9 @@
3030
import static java.util.Objects.requireNonNull;
3131

3232
import java.util.Map;
33-
import java.util.Objects;
3433
import java.util.UUID;
3534
import java.util.concurrent.CompletableFuture;
3635

37-
import org.jspecify.annotations.Nullable;
38-
3936
import com.fasterxml.jackson.core.JsonPointer;
4037
import com.fasterxml.jackson.databind.JsonNode;
4138
import com.google.common.collect.ImmutableMap;
@@ -172,11 +169,6 @@ Revision purgeAppIdentity(Author author, String appId) {
172169
}
173170

174171
CompletableFuture<Token> regenerateTokenSecret(Author author, String appId) {
175-
return regenerateTokenSecret(author, appId, null);
176-
}
177-
178-
CompletableFuture<Token> regenerateTokenSecret(Author author, String appId,
179-
@Nullable Token expectedToken) {
180172
requireNonNull(author, "author");
181173
requireNonNull(appId, "appId");
182174

@@ -193,19 +185,6 @@ CompletableFuture<Token> regenerateTokenSecret(Author author, String appId,
193185
"The app identity is already destroyed: " + appId);
194186
}
195187
throwIfInvalidType(appId, appIdentity, AppIdentityType.TOKEN);
196-
if (expectedToken != null &&
197-
!expectedToken.creation().equals(appIdentity.creation())) {
198-
// The token the caller was authorized for has been recreated in the meantime.
199-
throw new ChangeConflictException(
200-
"The app identity has been recreated concurrently: " + appId);
201-
}
202-
if (expectedToken != null &&
203-
!Objects.equals(expectedToken.secret(), ((Token) appIdentity).secret())) {
204-
// Another regeneration has been committed in the meantime; failing loudly
205-
// prevents the caller from distributing a secret that will never work.
206-
throw new ChangeConflictException(
207-
"The secret has been regenerated concurrently: " + appId);
208-
}
209188
if (appIdentity.deactivation() == null) {
210189
// Regenerating the secret of an active token would break its clients with no
211190
// way to prepare, so the token must be deactivated first.

server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,20 +1175,6 @@ public CompletableFuture<Token> regenerateTokenSecret(Author author, String appI
11751175
return appIdentityService.regenerateTokenSecret(author, appId);
11761176
}
11771177

1178-
/**
1179-
* Regenerates the secret of the deactivated {@link Token} of the specified {@code appId} and
1180-
* returns the {@link Token} with the newly-generated secret. The token must be deactivated first
1181-
* and the new secret does not authenticate until the token is activated. The regeneration fails
1182-
* with a {@link ChangeConflictException} if the token is still active, or if the token does not
1183-
* match {@code expectedToken} anymore because it was recreated or regenerated after the caller
1184-
* was authorized.
1185-
*/
1186-
public CompletableFuture<Token> regenerateTokenSecret(Author author, String appId,
1187-
Token expectedToken) {
1188-
requireNonNull(expectedToken, "expectedToken");
1189-
return appIdentityService.regenerateTokenSecret(author, appId, expectedToken);
1190-
}
1191-
11921178
/**
11931179
* Returns an {@link AppIdentity} which has the specified {@code appId}.
11941180
*/

server/src/test/java/com/linecorp/centraldogma/server/metadata/MetadataServiceTest.java

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -617,56 +617,6 @@ void cannotRegenerateSecretOfDestroyedToken() {
617617
.hasStackTraceContaining("already destroyed");
618618
}
619619

620-
@Test
621-
void cannotRegenerateSecretOfRecreatedToken() {
622-
final MetadataService mds = newMetadataService(manager);
623-
624-
mds.createToken(author, app1).join();
625-
await().untilAsserted(() -> assertThat(mds.getAppIdentityRegistry().getOrDefault(app1, null))
626-
.isNotNull());
627-
final Token oldToken = (Token) mds.getAppIdentityRegistry().get(app1);
628-
629-
// Destroy, purge and recreate a token with the same application ID.
630-
mds.destroyToken(author, app1).join();
631-
mds.purgeAppIdentity(author, app1);
632-
mds.createToken(author, app1).join();
633-
await().untilAsserted(() -> assertThat(mds.getAppIdentityRegistry().get(app1).creation())
634-
.isNotEqualTo(oldToken.creation()));
635-
636-
// A regeneration authorized against the old token must not rotate the recreated one,
637-
// even before the recreated token is deactivated.
638-
assertThatThrownBy(() -> mds.regenerateTokenSecret(author, app1, oldToken).join())
639-
.hasCauseInstanceOf(ChangeConflictException.class)
640-
.hasStackTraceContaining("recreated concurrently");
641-
642-
// A regeneration with the matching token succeeds once it is deactivated.
643-
mds.deactivateToken(author, app1).join();
644-
await().untilAsserted(() -> assertThat(mds.getAppIdentityRegistry().get(app1).isActive())
645-
.isFalse());
646-
final Token newToken = (Token) mds.getAppIdentityRegistry().get(app1);
647-
final Token returned = mds.regenerateTokenSecret(author, app1, newToken).join();
648-
assertThat(returned.secret()).startsWith("appToken-");
649-
}
650-
651-
@Test
652-
void cannotRegenerateSecretConcurrently() {
653-
final MetadataService mds = newMetadataService(manager);
654-
655-
mds.createToken(author, app1).join();
656-
mds.deactivateToken(author, app1).join();
657-
await().untilAsserted(() -> assertThat(mds.getAppIdentityRegistry().get(app1).isActive())
658-
.isFalse());
659-
final Token snapshot = (Token) mds.getAppIdentityRegistry().get(app1);
660-
661-
// Another regeneration is committed after the caller was authorized against the snapshot.
662-
mds.regenerateTokenSecret(author, app1).join();
663-
664-
// The stale caller fails loudly instead of receiving a secret that will never work.
665-
assertThatThrownBy(() -> mds.regenerateTokenSecret(author, app1, snapshot).join())
666-
.hasCauseInstanceOf(ChangeConflictException.class)
667-
.hasStackTraceContaining("regenerated concurrently");
668-
}
669-
670620
@Test
671621
void cannotRegenerateSecretOfCertificate() {
672622
final MetadataService mds = newMetadataService(manager);

0 commit comments

Comments
 (0)