Skip to content

Commit

Permalink
feat: update gw2_api_permissions_bit_set in scheduled check
Browse files Browse the repository at this point in the history
Some API Tokens were added with the new wvw permission before it was implemented in gw2auth. This update patches the gw2_api_permissions_bit_set of those.

This will be rolled back once all active API Tokens were processed
  • Loading branch information
its-felix committed Aug 7, 2024
1 parent 27b3386 commit da21469
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 16 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.gw2auth</groupId>
<artifactId>oauth2-server</artifactId>
<version>1.76.1</version>
<version>1.77.0</version>
<packaging>jar</packaging>

<parent>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.gw2auth.oauth2.server.repository.gw2account.apitoken;

import com.gw2auth.oauth2.server.service.Gw2ApiPermission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
Expand All @@ -16,14 +17,17 @@ public class CustomGw2AccountApiTokenRepositoryImpl implements CustomGw2AccountA

private static final String QUERY_VALID = """
UPDATE gw2_account_api_tokens
SET last_valid_time = :last_valid_check_time, last_valid_check_time = :last_valid_check_time
SET last_valid_time = :last_valid_check_time,
last_valid_check_time = :last_valid_check_time,
gw2_api_permissions_bit_set = IF(COALESCE( :gw2_api_permissions_bit_set , 0) = 0, gw2_api_permissions_bit_set, :gw2_api_permissions_bit_set )
WHERE account_id = :account_id
AND gw2_account_id = :gw2_account_id
""";

private static final String QUERY_INVALID = """
UPDATE gw2_account_api_tokens
SET last_valid_check_time = :last_valid_check_time
SET last_valid_check_time = :last_valid_check_time,
gw2_api_permissions_bit_set = IF(COALESCE( :gw2_api_permissions_bit_set , 0) = 0, gw2_api_permissions_bit_set, :gw2_api_permissions_bit_set )
WHERE account_id = :account_id
AND gw2_account_id = :gw2_account_id
""";
Expand All @@ -45,6 +49,7 @@ public void updateApiTokensValid(Instant lastValidCheckTime, Collection<Gw2Accou
final SqlParameterSource sqlParameterSource = new MapSqlParameterSource(Map.of(
"account_id", entity.accountId(),
"gw2_account_id", entity.gw2AccountId(),
"gw2_api_permissions_bit_set", Optional.ofNullable(entity.gw2ApiPermissions()).map(Gw2ApiPermission::toBitSet).orElse(0),
"last_valid_check_time", Timestamp.from(lastValidCheckTime)
));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Gw2AccountApiTokenEntity save(@Param("account_id") UUID accountId,
Optional<Gw2AccountApiTokenEntity> findByAccountIdAndGw2AccountId(@Param("account_id") UUID accountId, @Param("gw2_account_id") UUID gw2AccountId);

@Query("""
SELECT tk.account_id, tk.gw2_account_id, acc.gw2_account_name, tk.gw2_api_token
SELECT tk.account_id, tk.gw2_account_id, acc.gw2_account_name, tk.gw2_api_permissions_bit_set, tk.gw2_api_token
FROM gw2_account_api_tokens tk
INNER JOIN gw2_accounts acc
ON tk.account_id = acc.account_id AND tk.gw2_account_id = acc.gw2_account_id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.gw2auth.oauth2.server.repository.gw2account.apitoken;

import com.gw2auth.oauth2.server.service.Gw2ApiPermission;

import java.util.Set;
import java.util.UUID;

public record Gw2AccountApiTokenValidUpdateEntity(UUID accountId, UUID gw2AccountId, boolean isValid) {
public record Gw2AccountApiTokenValidUpdateEntity(UUID accountId, UUID gw2AccountId, Set<Gw2ApiPermission> gw2ApiPermissions, boolean isValid) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
public record Gw2AccountRefreshEntity(@Column("account_id") UUID accountId,
@Column("gw2_account_id") UUID gw2AccountId,
@Column("gw2_account_name") String gw2AccountName,
@Column("gw2_api_permissions_bit_set") int gw2ApiPermissionsBitSet,
@Column("gw2_api_token") String gw2ApiToken) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.gw2auth.oauth2.server.repository.gw2account.apitoken.Gw2AccountRefreshEntity;
import com.gw2auth.oauth2.server.repository.gw2account.apitoken.Gw2AccountApiTokenValidUpdateEntity;
import com.gw2auth.oauth2.server.service.Clocked;
import com.gw2auth.oauth2.server.service.Gw2ApiPermission;
import com.gw2auth.oauth2.server.service.gw2.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -59,7 +60,7 @@ public void setClock(Clock clock) {
@Override
public void updateApiTokensValid(Instant lastValidCheckTime, Collection<Gw2AccountApiTokenValidUpdate> _updates) {
final List<Gw2AccountApiTokenValidUpdateEntity> updates = _updates.stream()
.map((v) -> new Gw2AccountApiTokenValidUpdateEntity(v.accountId(), v.gw2AccountId(), v.isValid()))
.map((v) -> new Gw2AccountApiTokenValidUpdateEntity(v.accountId(), v.gw2AccountId(), null, v.isValid()))
.toList();

this.gw2AccountApiTokenRepository.updateApiTokensValid(lastValidCheckTime, updates);
Expand All @@ -83,14 +84,28 @@ public void refreshTokenValidityAndAccountName() {

for (Gw2AccountRefreshEntity apiTokenValidCheckEntity : tokensToCheck) {
Boolean isValidState;
Set<Gw2ApiPermission> gw2ApiPermissions;
try {
final Gw2TokenInfo gw2TokenInfo = this.gw2ApiService.getTokenInfo(apiTokenValidCheckEntity.gw2ApiToken());
final Gw2Account gw2Account = this.gw2ApiService.getAccount(apiTokenValidCheckEntity.gw2ApiToken());
isValidState = true;
gw2ApiPermissions = gw2TokenInfo.permissions();
validCount++;

final boolean hasAccountNameChanged = !gw2Account.name().equals(apiTokenValidCheckEntity.gw2AccountName());
nameUpdateEntities.add(new Gw2AccountNameUpdateEntity(apiTokenValidCheckEntity.accountId(), apiTokenValidCheckEntity.gw2AccountId(), gw2Account.name(), hasAccountNameChanged));

final Set<Gw2ApiPermission> prevGw2ApiPermissions = Gw2ApiPermission.fromBitSet(apiTokenValidCheckEntity.gw2ApiPermissionsBitSet());
if (!gw2ApiPermissions.equals(prevGw2ApiPermissions)) {
LOG.info(
"gw2 api permissions changed for gw2_account_id={} on account_id={}; old={} new={}",
apiTokenValidCheckEntity.gw2AccountId(),
apiTokenValidCheckEntity.accountId(),
prevGw2ApiPermissions,
gw2ApiPermissions
);
}

if (hasAccountNameChanged) {
LOG.info(
"gw2 account name changed for gw2_account_id={} on account_id={}; old={} new={}",
Expand All @@ -103,14 +118,21 @@ public void refreshTokenValidityAndAccountName() {
}
} catch (InvalidApiTokenException e) {
isValidState = false;
gw2ApiPermissions = null;
invalidCount++;
} catch (Gw2ApiServiceException e) {
isValidState = null;
gw2ApiPermissions = null;
unknownCount++;
}

if (isValidState != null) {
validUpdateEntities.add(new Gw2AccountApiTokenValidUpdateEntity(apiTokenValidCheckEntity.accountId(), apiTokenValidCheckEntity.gw2AccountId(), isValidState));
validUpdateEntities.add(new Gw2AccountApiTokenValidUpdateEntity(
apiTokenValidCheckEntity.accountId(),
apiTokenValidCheckEntity.gw2AccountId(),
gw2ApiPermissions,
isValidState
));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.EnumSet;
import java.util.Map;
import java.util.UUID;
import java.util.*;

import static com.gw2auth.oauth2.server.Assertions.assertInstantEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -92,6 +90,25 @@ public void checkTokenValidityWithUpdatedGw2AccountNames(SessionHandle sessionHa
assertEquals("Felix.9127 (Main)", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());

// prepare the mock server for the upcoming tokeninfo request
this.gw2RestServer.reset();
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/tokeninfo")))
.andExpect(method(HttpMethod.GET))
.andExpect(MockRestRequestMatchers.header("Authorization", new IsEqual<>("Bearer " + gw2ApiToken)))
.andRespond((request) -> {
final JSONObject responseJson = new JSONObject(Map.of(
"name", "TokenName",
"permissions", List.of("account")
));
final MockClientHttpResponse response = new MockClientHttpResponse(
responseJson.toString().getBytes(StandardCharsets.UTF_8),
HttpStatus.OK
);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

return response;
});

// prepare the mock server for the upcoming account request
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/account")))
.andExpect(method(HttpMethod.GET))
Expand Down Expand Up @@ -122,8 +139,92 @@ public void checkTokenValidityWithUpdatedGw2AccountNames(SessionHandle sessionHa
assertEquals("Felix.9127", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());


// verify the permissions were not changed
final Gw2AccountApiTokenEntity gw2AccountApiTokenEntity = this.gw2AccountApiTokenRepository.findByAccountIdAndGw2AccountId(accountId, gw2AccountId).orElseThrow();
assertEquals(Gw2ApiPermission.toBitSet(Set.of(Gw2ApiPermission.ACCOUNT)), gw2AccountApiTokenEntity.gw2ApiPermissionsBitSet());

// verify the token was marked as valid
assertInstantEquals(now, gw2AccountApiTokenEntity.lastValidCheckTime());
assertInstantEquals(now, gw2AccountApiTokenEntity.lastValidTime());
}

@ParameterizedTest
@WithGw2AuthLogin
public void checkTokenValidityWithUpdatedGw2ApiPermissions(SessionHandle sessionHandle) {
final UUID accountId = this.testHelper.getAccountIdForCookie(sessionHandle).orElseThrow();
final UUID gw2AccountId = UUID.randomUUID();
final String gw2ApiToken = TestHelper.randomRootToken();

// create new token (also creates the account entity)
this.testHelper.createApiToken(
accountId,
gw2AccountId,
gw2ApiToken,
EnumSet.of(Gw2ApiPermission.ACCOUNT),
"Felix.9127",
"Felix.9127 (Main)"
);

// verify the expected values are present in the DB
Gw2AccountEntity gw2AccountEntity = this.gw2AccountRepository.findByAccountIdAndGw2AccountId(accountId, gw2AccountId).orElseThrow();
assertEquals("Felix.9127", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());

// prepare the mock server for the upcoming tokeninfo request
this.gw2RestServer.reset();
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/tokeninfo")))
.andExpect(method(HttpMethod.GET))
.andExpect(MockRestRequestMatchers.header("Authorization", new IsEqual<>("Bearer " + gw2ApiToken)))
.andRespond((request) -> {
final JSONObject responseJson = new JSONObject(Map.of(
"name", "TokenName",
"permissions", List.of("account", "wvw")
));
final MockClientHttpResponse response = new MockClientHttpResponse(
responseJson.toString().getBytes(StandardCharsets.UTF_8),
HttpStatus.OK
);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

return response;
});

// prepare the mock server for the upcoming account request
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/account")))
.andExpect(method(HttpMethod.GET))
.andExpect(MockRestRequestMatchers.header("Authorization", new IsEqual<>("Bearer " + gw2ApiToken)))
.andRespond((request) -> {
final JSONObject responseJson = new JSONObject(Map.of(
"id", gw2AccountId.toString(),
"name", "Felix.9127"
));
final MockClientHttpResponse response = new MockClientHttpResponse(
responseJson.toString().getBytes(StandardCharsets.UTF_8),
HttpStatus.OK
);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

return response;
});

// simulate 4hrs passed (token verification happens every 3h - see application.yml for test)
final Instant now = Instant.now().plus(Duration.ofHours(4L));
this.gw2AuthClockedExtension.setClock(Clock.fixed(now, ZoneId.systemDefault()));

// trigger validity check
this.gw2AccountApiTokenService.refreshTokenValidityAndAccountName();

// verify the account name was not updated
gw2AccountEntity = this.gw2AccountRepository.findByAccountIdAndGw2AccountId(accountId, gw2AccountId).orElseThrow();
assertEquals("Felix.9127", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());

// verify the permissions were updated
final Gw2AccountApiTokenEntity gw2AccountApiTokenEntity = this.gw2AccountApiTokenRepository.findByAccountIdAndGw2AccountId(accountId, gw2AccountId).orElseThrow();
assertEquals(Gw2ApiPermission.toBitSet(Set.of(Gw2ApiPermission.ACCOUNT, Gw2ApiPermission.WVW)), gw2AccountApiTokenEntity.gw2ApiPermissionsBitSet());

// verify the token was marked as valid
assertInstantEquals(now, gw2AccountApiTokenEntity.lastValidCheckTime());
assertInstantEquals(now, gw2AccountApiTokenEntity.lastValidTime());
}
Expand All @@ -150,8 +251,9 @@ public void checkTokenValidityWithFailingGw2ApiRequest(SessionHandle sessionHand
assertEquals("Felix.9127 (Main)", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());

// prepare the mock server for the upcoming account request
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/account")))
// prepare the mock server for the upcoming tokeninfo request
this.gw2RestServer.reset();
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/tokeninfo")))
.andExpect(method(HttpMethod.GET))
.andExpect(MockRestRequestMatchers.header("Authorization", new IsEqual<>("Bearer " + gw2ApiToken)))
.andRespond((request) -> {
Expand All @@ -176,8 +278,11 @@ public void checkTokenValidityWithFailingGw2ApiRequest(SessionHandle sessionHand
assertEquals("Felix.9127 (Main)", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());

// verify neither times were updated
// verify the permissions were not changed
final Gw2AccountApiTokenEntity gw2AccountApiTokenEntityNew = this.gw2AccountApiTokenRepository.findByAccountIdAndGw2AccountId(accountId, gw2AccountId).orElseThrow();
assertEquals(gw2AccountApiTokenEntityOld.gw2ApiPermissionsBitSet(), gw2AccountApiTokenEntityNew.gw2ApiPermissionsBitSet());

// verify neither times were updated
assertInstantEquals(gw2AccountApiTokenEntityOld.lastValidCheckTime(), gw2AccountApiTokenEntityNew.lastValidCheckTime());
assertInstantEquals(gw2AccountApiTokenEntityOld.lastValidTime(), gw2AccountApiTokenEntityNew.lastValidTime());
}
Expand All @@ -204,8 +309,9 @@ public void checkTokenValidityWithInvalidApiToken(SessionHandle sessionHandle) {
assertEquals("Felix.9127 (Main)", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());

// prepare the mock server for the upcoming account request
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/account")))
// prepare the mock server for the upcoming tokeninfo request
this.gw2RestServer.reset();
this.gw2RestServer.expect(times(1), requestTo(new StringStartsWith("/v2/tokeninfo")))
.andExpect(method(HttpMethod.GET))
.andExpect(MockRestRequestMatchers.header("Authorization", new IsEqual<>("Bearer " + gw2ApiToken)))
.andRespond((request) -> {
Expand All @@ -230,8 +336,11 @@ public void checkTokenValidityWithInvalidApiToken(SessionHandle sessionHandle) {
assertEquals("Felix.9127 (Main)", gw2AccountEntity.gw2AccountName());
assertEquals("Felix.9127 (Main)", gw2AccountEntity.displayName());

// verify only the check time was updated
// verify the permissions were not changed
final Gw2AccountApiTokenEntity gw2AccountApiTokenEntityNew = this.gw2AccountApiTokenRepository.findByAccountIdAndGw2AccountId(accountId, gw2AccountId).orElseThrow();
assertEquals(gw2AccountApiTokenEntityOld.gw2ApiPermissionsBitSet(), gw2AccountApiTokenEntityNew.gw2ApiPermissionsBitSet());

// verify only the check time was updated
assertInstantEquals(now, gw2AccountApiTokenEntityNew.lastValidCheckTime());
assertInstantEquals(gw2AccountApiTokenEntityOld.lastValidTime(), gw2AccountApiTokenEntityNew.lastValidTime());
}
Expand Down

0 comments on commit da21469

Please sign in to comment.