Update the lastUsed timestamp of PAT at least one minute apart (#4671)

#### What type of PR is this?

/kind bug
/area core

#### What this PR does / why we need it:

After PAT mechanism implemented by <https://github.com/halo-dev/halo/pull/4598>, if we use the same PAT to request endpoints concurrently, we may encounter an error like the screenshot below:

<img width="1920" alt="image" src="https://github.com/halo-dev/halo/assets/16865714/30899a0c-ad98-44a1-ae7d-0eda603945f0">

This PR fixes the problem introduced by <https://github.com/halo-dev/halo/pull/4598>.

We update the lastUsed timestamp of PAT at least one minute apart and with retry.

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/4678/head
John Niang 2023-09-28 08:34:18 -05:00 committed by GitHub
parent 9454f445a5
commit 37ddccc612
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 33 additions and 9 deletions

View File

@ -6,7 +6,9 @@ import static run.halo.app.security.authentication.pat.PatServerWebExchangeMatch
import com.nimbusds.jwt.JWTClaimNames;
import java.time.Clock;
import java.time.Duration;
import java.util.Objects;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.core.Authentication;
@ -17,6 +19,7 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtRea
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.security.PersonalAccessToken;
@ -24,6 +27,11 @@ import run.halo.app.security.authentication.jwt.JwtScopesAndRolesGrantedAuthorit
public class PatAuthenticationManager implements ReactiveAuthenticationManager {
/**
* Minimal duration gap of personal access token update.
*/
private static final Duration MIN_UPDATE_GAP = Duration.ofMinutes(1);
private final ReactiveAuthenticationManager delegate;
private final ReactiveExtensionClient client;
@ -78,17 +86,33 @@ public class PatAuthenticationManager implements ReactiveAuthenticationManager {
return client.fetch(PersonalAccessToken.class, patName)
.switchIfEmpty(
Mono.error(() -> new DisabledException("Personal access token has been deleted.")))
.flatMap(pat -> patChecks(pat, jwtId)
.then(updateLastUsed(pat))
.then()
);
.flatMap(pat -> patChecks(pat, jwtId).and(updateLastUsed(patName)));
}
private Mono<PersonalAccessToken> updateLastUsed(PersonalAccessToken pat) {
return Mono.defer(() -> {
pat.getSpec().setLastUsed(clock.instant());
return client.update(pat);
});
private Mono<Void> updateLastUsed(String patName) {
// we try our best to update the last used timestamp.
// the now should be outside the retry cycle because we don't want a fresh timestamp at
// every retry.
var now = clock.instant();
return Mono.defer(
// we have to obtain a fresh PAT and retry the update.
() -> client.fetch(PersonalAccessToken.class, patName)
.filter(pat -> {
var lastUsed = pat.getSpec().getLastUsed();
if (lastUsed == null) {
return true;
}
var diff = Duration.between(lastUsed, now);
return !diff.minus(MIN_UPDATE_GAP).isNegative();
})
.doOnNext(pat -> pat.getSpec().setLastUsed(now))
.flatMap(client::update)
)
.retryWhen(Retry.backoff(3, Duration.ofMillis(50))
.filter(OptimisticLockingFailureException.class::isInstance))
.onErrorComplete()
.then();
}
private Mono<Void> patChecks(PersonalAccessToken pat, String tokenId) {