Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public class Constants {
public static final String B3_TRACE_ID_HEADER = "X-B3-TraceId";
public static final String B3_SPAN_ID_HEADER = "X-B3-SpanId";

public static final int TOKEN_SERVICE_DELETION_CORE_POOL_SIZE = 1;
public static final int TOKEN_SERVICE_DELETION_MAXIMUM_POOL_SIZE = 3;
public static final int TOKEN_SERVICE_DELETION_KEEP_ALIVE_THREAD_IN_SECONDS = 30;

protected Constants() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
public final class Messages {

// Exception messages
public static final String NO_VALID_TOKEN_FOUND = "No valid access token was found for user \"{0}\"";
public static final String NO_VALID_TOKEN_FOUND = "No valid access token was found for user guid \"{0}\"";
public static final String CANT_CREATE_CLIENT = "Could not create client";
public static final String CANT_CREATE_CLIENT_FOR_SPACE_ID = "Could not create client in space with guid \"{0}\"";
public static final String UNAUTHORISED_OPERATION_ORG_SPACE = "Not authorized to perform operation \"{0}\" in organization \"{1}\" and space \"{2}\"";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

import java.time.Duration;

import com.sap.cloudfoundry.client.facade.CloudControllerClient;
import com.sap.cloudfoundry.client.facade.CloudOperationException;
import jakarta.inject.Inject;
import jakarta.inject.Named;

import org.cloudfoundry.multiapps.common.SLException;
import org.cloudfoundry.multiapps.controller.core.Messages;
import org.cloudfoundry.multiapps.controller.core.model.CachedMap;
import org.cloudfoundry.multiapps.controller.core.security.token.TokenService;
import org.springframework.beans.factory.DisposableBean;

import com.sap.cloudfoundry.client.facade.CloudControllerClient;
import com.sap.cloudfoundry.client.facade.CloudOperationException;

@Named
public class CloudControllerClientProvider implements DisposableBean {

Expand All @@ -25,59 +23,64 @@ public class CloudControllerClientProvider implements DisposableBean {
private final CachedMap<String, CloudControllerClient> clients = new CachedMap<>(Duration.ofMinutes(30));

/**
* Returns a client for the specified user name and space id by either getting it from the clients cache or creating a new one.
* Returns a client for the specified user guid and space id by either getting it from the clients cache or creating a new one.
*
* @param userName the user name associated with the client
* @param userName the username associated with the client
* @param userGuid the userGuid associated with the client
* @param spaceGuid the space guid associated with the client
* @param correlationId of the process which is used to tag HTTP requests
* @return a CF client for the specified access token, organization, and space
*/
public CloudControllerClient getControllerClient(String userName, String spaceGuid, String correlationId) {
public CloudControllerClient getControllerClient(String userName, String userGuid, String spaceGuid, String correlationId) {
try {
return getClientFromCache(userName, spaceGuid, correlationId);
return getClientFromCache(userName, userGuid, spaceGuid, correlationId);
} catch (CloudOperationException e) {
throw new SLException(e, Messages.CANT_CREATE_CLIENT_FOR_SPACE_ID, spaceGuid);
}
}

/**
* Returns a client for the specified user name and space id by either getting it from the clients cache or creating a new one.
* Returns a client for the specified username and space id by either getting it from the clients cache or creating a new one.
*
* @param userName the user name associated with the client
* @param userName the username associated with the client
* @param userGuid the userGuid associated with the client
* @param spaceGuid the space guid associated with the client
* @return a CF client for the specified access token, organization, and space
*/
public CloudControllerClient getControllerClientWithNoCorrelation(String userName, String spaceGuid) {
public CloudControllerClient getControllerClientWithNoCorrelation(String userName, String userGuid, String spaceGuid) {
try {
return getClientFromCacheWithNoCorrelation(userName, spaceGuid);
return getClientFromCacheWithNoCorrelation(userName, userGuid, spaceGuid);
} catch (CloudOperationException e) {
throw new SLException(e, Messages.CANT_CREATE_CLIENT_FOR_SPACE_ID, spaceGuid);
}
}

/**
* Releases the client for the specified user name and space id by removing it from the clients cache.
* Releases the client for the specified username and space id by removing it from the clients cache.
*
* @param userName the user name associated with the client
* @param userGuid the userGuid associated with the client
* @param spaceGuid the space id associated with the client
*/
public void releaseClient(String userName, String spaceGuid) {
clients.remove(getKey(spaceGuid, userName));
public void releaseClient(String userGuid, String spaceGuid) {
clients.remove(getKey(spaceGuid, userGuid, null));
}

private CloudControllerClient getClientFromCacheWithNoCorrelation(String userName, String spaceId) {
String key = getKey(spaceId, userName);
return clients.computeIfAbsent(key,
() -> clientFactory.createClient(tokenService.getToken(userName), spaceId, null));
private CloudControllerClient getClientFromCacheWithNoCorrelation(String userName, String userGuid, String spaceId) {
String key = getKey(spaceId, userGuid, userName);
return clients.computeIfAbsent(key, () -> clientFactory.createClient(tokenService.getToken(userName, userGuid), spaceId, null));
}

private CloudControllerClient getClientFromCache(String userName, String spaceId, String correlationId) {
String key = getKey(spaceId, userName);
private CloudControllerClient getClientFromCache(String userName, String userGuid, String spaceId, String correlationId) {
String key = getKey(spaceId, userGuid, userName);
return clients.computeIfAbsent(key,
() -> clientFactory.createClient(tokenService.getToken(userName), spaceId, correlationId));
() -> clientFactory.createClient(tokenService.getToken(userName, userGuid), spaceId, correlationId));
}

private String getKey(String spaceGuid, String username) {
private String getKey(String spaceGuid, String userGuid, String username) {
if (userGuid != null) {
return spaceGuid + "|" + userGuid;
}
// TODO: Remove this branch when userGuid is guaranteed to be non-null(In the next release after introduction of userGuid)
return spaceGuid + "|" + username;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@
import java.time.Instant;
import java.time.temporal.ChronoUnit;

import com.sap.cloudfoundry.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
import com.sap.cloudfoundry.client.facade.oauth2.OAuthClient;
import org.cloudfoundry.multiapps.controller.client.util.TokenProperties;
import org.cloudfoundry.multiapps.controller.core.Messages;
import org.cloudfoundry.multiapps.controller.core.security.token.TokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.reactive.function.client.WebClient;

import com.sap.cloudfoundry.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
import com.sap.cloudfoundry.client.facade.oauth2.OAuthClient;

public class OAuthClientExtended extends OAuthClient {

private static final Logger LOGGER = LoggerFactory.getLogger(OAuthClientExtended.class);
Expand All @@ -35,10 +34,11 @@ public OAuth2AccessTokenWithAdditionalInfo getToken() {
.isBefore(Instant.now()
.plus(120, ChronoUnit.SECONDS))) {
TokenProperties tokenProperties = TokenProperties.fromToken(token);
token = tokenService.getToken(tokenProperties.getUserName());
LOGGER.info(MessageFormat.format(Messages.RETRIEVED_TOKEN_FOR_USER_WITH_GUID_0_WITH_EXPIRATION_TIME_1, tokenProperties.getUserId(),
token.getOAuth2AccessToken()
.getExpiresAt()));
token = tokenService.getToken(tokenProperties.getUserName(), tokenProperties.getUserId());
LOGGER.info(
MessageFormat.format(Messages.RETRIEVED_TOKEN_FOR_USER_WITH_GUID_0_WITH_EXPIRATION_TIME_1, tokenProperties.getUserId(),
token.getOAuth2AccessToken()
.getExpiresAt()));
}
return token;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.sap.cloudfoundry.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.cloudfoundry.multiapps.controller.client.util.TokenProperties;
import org.cloudfoundry.multiapps.controller.core.Constants;
import org.cloudfoundry.multiapps.controller.core.Messages;
import org.cloudfoundry.multiapps.controller.core.model.CachedMap;
import org.cloudfoundry.multiapps.controller.core.security.token.parsers.TokenParserChain;
Expand All @@ -20,11 +25,6 @@
import org.cloudfoundry.multiapps.controller.persistence.services.AccessTokenService;
import org.springframework.beans.factory.DisposableBean;

import com.sap.cloudfoundry.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo;

import jakarta.inject.Inject;
import jakarta.inject.Named;

/**
* Provides functionality for persisting, updating and removing tokens from a token store
*/
Expand All @@ -35,11 +35,10 @@ public class TokenService implements DisposableBean {
private final TokenParserChain tokenParserChain;
private final Duration tokenExpirationTime = Duration.ofMinutes(10);
private final CachedMap<String, OAuth2AccessTokenWithAdditionalInfo> cachedTokens = new CachedMap<>(tokenExpirationTime);
private final ExecutorService threadPoolForTokensDeletion = new ThreadPoolExecutor(1,
3,
30,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
private final ExecutorService threadPoolForTokensDeletion = new ThreadPoolExecutor(Constants.TOKEN_SERVICE_DELETION_CORE_POOL_SIZE,
Constants.TOKEN_SERVICE_DELETION_MAXIMUM_POOL_SIZE,
Constants.TOKEN_SERVICE_DELETION_KEEP_ALIVE_THREAD_IN_SECONDS,
TimeUnit.SECONDS, new LinkedBlockingQueue<>());

@Inject
public TokenService(AccessTokenService accessTokenService, TokenParserChain tokenParserChain) {
Expand All @@ -51,21 +50,19 @@ public TokenService(AccessTokenService accessTokenService, TokenParserChain toke
* Chooses a token among all tokens for this user in the access token table.
*
* @param username the username
* @param userGuid the userGuid
* @return the latest token, or throw an exception if token is not found
*/
public OAuth2AccessTokenWithAdditionalInfo getToken(String username) {
OAuth2AccessTokenWithAdditionalInfo cachedAccessToken = cachedTokens.get(username);
if (shouldUseCachedToken(cachedAccessToken)) {
return cachedAccessToken;
}
List<AccessToken> accessTokens = getSortedAccessTokensByUsername(username);
if (accessTokens.isEmpty()) {
throw new IllegalStateException(MessageFormat.format(Messages.NO_VALID_TOKEN_FOUND, username));
public OAuth2AccessTokenWithAdditionalInfo getToken(String username, String userGuid) {
Copy link
Copy Markdown
Contributor

@s-yonkov-yonkov s-yonkov-yonkov Jun 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code suggestion:

    if (userGuid != null) {
        OAuth2AccessTokenWithAdditionalInfo cached = cachedTokens.get(userGuid);
        if (shouldUseCachedToken(cached)) {
            return cached;
        }

        List<AccessToken> tokensByGuid = getSortedAccessTokensByUserGuid(userGuid);
        if (!tokensByGuid.isEmpty()) {
            return cacheAndReturnLatest(tokensByGuid);
        }
    }

    List<AccessToken> tokensByUsername = getSortedAccessTokensByUsername(username);
    if (tokensByUsername.isEmpty()) {
        throw new IllegalStateException(MessageFormat.format(Messages.NO_VALID_TOKEN_FOUND, userGuid));
    }

    return cacheAndReturnLatest(tokensByUsername);
}

private OAuth2AccessTokenWithAdditionalInfo cacheAndReturnLatest(List<AccessToken> tokens) {
    OAuth2AccessTokenWithAdditionalInfo latest = getLatestToken(tokens);
    String userId = (String) latest.getAdditionalInfo().get(TokenProperties.USER_ID_KEY);
    cachedTokens.put(userId, latest);

    if (tokens.size() > 1) {
        deleteTokens(tokens.subList(1, tokens.size()));
    }

    return latest;
}

if (userGuid != null) {
OAuth2AccessTokenWithAdditionalInfo cachedAccessToken = cachedTokens.get(userGuid);
if (shouldUseCachedToken(cachedAccessToken)) {
return cachedAccessToken;
}
return getLatestAccessTokenByUserGuid(userGuid);
}
OAuth2AccessTokenWithAdditionalInfo tokenByUser = getLatestToken(accessTokens);
cachedTokens.put(username, tokenByUser);
deleteTokens(accessTokens.subList(1, accessTokens.size()));
return tokenByUser;
// TODO: If no tokens are found for the userGuid, try to find tokens by username. This is temporary and should be removed in the next release.
return getLatestAccessTokenByUsername(username);
}

private boolean shouldUseCachedToken(OAuth2AccessTokenWithAdditionalInfo cachedAccessToken) {
Expand All @@ -75,9 +72,42 @@ private boolean shouldUseCachedToken(OAuth2AccessTokenWithAdditionalInfo cachedA
.plus(120, ChronoUnit.SECONDS));
}

private List<AccessToken> getSortedAccessTokensByUsername(String userName) {
private OAuth2AccessTokenWithAdditionalInfo getLatestAccessTokenByUserGuid(String userGuid) {
List<AccessToken> tokensByGuid = getSortedAccessTokensByUserGuid(userGuid);
if (tokensByGuid.isEmpty()) {
throw new IllegalStateException(MessageFormat.format(Messages.NO_VALID_TOKEN_FOUND, userGuid));
}
OAuth2AccessTokenWithAdditionalInfo latestToken = getLatestToken(tokensByGuid);
addTokenToCache(latestToken, tokensByGuid);
return latestToken;
}

private List<AccessToken> getSortedAccessTokensByUserGuid(String userGuid) {
return accessTokenService.createQuery()
.userGuid(userGuid)
.orderByExpiresAt(OrderDirection.DESCENDING)
.list();
}

private void addTokenToCache(OAuth2AccessTokenWithAdditionalInfo token, List<AccessToken> accessTokens) {
cachedTokens.put((String) token.getAdditionalInfo()
.get(TokenProperties.USER_ID_KEY), token);
if (accessTokens.size() > 1) {
deleteTokens(accessTokens.subList(1, accessTokens.size()));
}
}

private OAuth2AccessTokenWithAdditionalInfo getLatestAccessTokenByUsername(String username) {
List<AccessToken> tokensByUsername = getSortedAccessTokensByUsername(username);
if (tokensByUsername.isEmpty()) {
throw new IllegalStateException(MessageFormat.format(Messages.NO_VALID_TOKEN_FOUND, username));
}
return getLatestToken(tokensByUsername);
}

private List<AccessToken> getSortedAccessTokensByUsername(String username) {
return accessTokenService.createQuery()
.username(userName)
.username(username)
.orderByExpiresAt(OrderDirection.DESCENDING)
.list();
}
Expand Down
Loading