Skip to content

Commit

Permalink
SLCORE-1032 Wrong token error during sync (#1187)
Browse files Browse the repository at this point in the history
  • Loading branch information
kirill-knize-sonarsource authored Jan 21, 2025
1 parent 48c8438 commit faedf81
Show file tree
Hide file tree
Showing 44 changed files with 673 additions and 298 deletions.
5 changes: 5 additions & 0 deletions API_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Breaking changes

* Add new method `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#invalidToken` to notify client that WebAPI calls to SQS/SQC fails due to wrong token
* Client can implement this method to offer user to change credentials for the connection to fix the problem
* For now notification is being sent only for 403 Forbidden HTTP response code since it's corresponds to malformed/wrong token and ignores 401 Unauthorized response code since it's a user permissions problem that has to be addressed on the server
* Also once notification sent, backend doesn't attempt to send any requests to server anymore until credentials changed

* Removed `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient#didRaiseIssue` and associated types. See `raiseIssues` and `raiseHotspots` instead.
* Removed `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer#getIssueTrackingService` and associated types. Tracking is managed by the backend.
* Removed `org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcServer#getSecurityHotspotMatchingService` and associated types. Tracking is managed by the backend.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@

import java.net.URI;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
import org.sonarsource.sonarlint.core.connection.ServerConnection;
import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor;
import org.sonarsource.sonarlint.core.http.ConnectionAwareHttpClientProvider;
import org.sonarsource.sonarlint.core.http.HttpClient;
import org.sonarsource.sonarlint.core.http.HttpClientProvider;
import org.sonarsource.sonarlint.core.repository.connection.ConnectionConfigurationRepository;
import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcClient;
import org.sonarsource.sonarlint.core.rpc.protocol.SonarLintRpcErrorCode;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarCloudConnectionDto;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.connection.common.TransientSonarQubeConnectionDto;
Expand All @@ -47,19 +51,21 @@

@Named
@Singleton
public class ServerApiProvider {
public class ConnectionManager {

private static final SonarLintLogger LOG = SonarLintLogger.get();
private final ConnectionConfigurationRepository connectionRepository;
private final ConnectionAwareHttpClientProvider awareHttpClientProvider;
private final HttpClientProvider httpClientProvider;
private final SonarLintRpcClient client;
private final URI sonarCloudUri;

public ServerApiProvider(ConnectionConfigurationRepository connectionRepository, ConnectionAwareHttpClientProvider awareHttpClientProvider, HttpClientProvider httpClientProvider,
SonarCloudActiveEnvironment sonarCloudActiveEnvironment) {
public ConnectionManager(ConnectionConfigurationRepository connectionRepository, ConnectionAwareHttpClientProvider awareHttpClientProvider, HttpClientProvider httpClientProvider,
SonarCloudActiveEnvironment sonarCloudActiveEnvironment, SonarLintRpcClient client) {
this.connectionRepository = connectionRepository;
this.awareHttpClientProvider = awareHttpClientProvider;
this.httpClientProvider = httpClientProvider;
this.client = client;
this.sonarCloudUri = sonarCloudActiveEnvironment.getUri();
}

Expand Down Expand Up @@ -100,7 +106,7 @@ public ServerApi getServerApi(String baseUrl, @Nullable String organization, Str
return new ServerApi(params, httpClientProvider.getHttpClientWithPreemptiveAuth(token, isBearerSupported));
}

public ServerApi getServerApiOrThrow(String connectionId) {
private ServerApi getServerApiOrThrow(String connectionId) {
var params = connectionRepository.getEndpointParams(connectionId);
if (params.isEmpty()) {
var error = new ResponseError(SonarLintRpcErrorCode.CONNECTION_NOT_FOUND, "Connection '" + connectionId + "' is gone", connectionId);
Expand Down Expand Up @@ -137,4 +143,51 @@ private HttpClient getClientFor(EndpointParams params, Either<TokenDto, Username
userPass -> httpClientProvider.getHttpClientWithPreemptiveAuth(userPass.getUsername(), userPass.getPassword()));
}

/**
* Throws ResponseErrorException if connection with provided ID is not found in ConnectionConfigurationRepository
*/
public ServerConnection getConnectionOrThrow(String connectionId) {
var serverApi = getServerApiOrThrow(connectionId);
return new ServerConnection(connectionId, serverApi, client);
}

/**
* Returns empty Optional if connection with provided ID is not found in ConnectionConfigurationRepository
*/
public Optional<ServerConnection> tryGetConnection(String connectionId) {
return getServerApi(connectionId)
.map(serverApi -> new ServerConnection(connectionId, serverApi, client));
}

/**
* Should be used for WebAPI requests without an authentication
*/
public Optional<ServerConnection> tryGetConnectionWithoutCredentials(String connectionId) {
return getServerApiWithoutCredentials(connectionId)
.map(serverApi -> new ServerConnection(connectionId, serverApi, client));
}

public ServerApi getTransientConnection(String token,@Nullable String organization, String baseUrl) {
return getServerApi(baseUrl, organization, token);
}

public void withValidConnection(String connectionId, Consumer<ServerApi> serverApiConsumer) {
getValidConnection(connectionId).ifPresent(connection -> connection.withClientApi(serverApiConsumer));
}

public <T> Optional<T> withValidConnectionAndReturn(String connectionId, Function<ServerApi, T> serverApiConsumer) {
return getValidConnection(connectionId).map(connection -> connection.withClientApiAndReturn(serverApiConsumer));
}

public <T> Optional<T> withValidConnectionFlatMapOptionalAndReturn(String connectionId, Function<ServerApi, Optional<T>> serverApiConsumer) {
return getValidConnection(connectionId).map(connection -> connection.withClientApiAndReturn(serverApiConsumer)).flatMap(Function.identity());
}

private Optional<ServerConnection> getValidConnection(String connectionId) {
return tryGetConnection(connectionId).filter(ServerConnection::isValid)
.or(() -> {
LOG.debug("Connection '{}' is invalid", connectionId);
return Optional.empty();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,23 @@ public class ConnectionService {
private final ApplicationEventPublisher applicationEventPublisher;
private final ConnectionConfigurationRepository repository;
private final URI sonarCloudUri;
private final ServerApiProvider serverApiProvider;
private final ConnectionManager connectionManager;
private final TokenGeneratorHelper tokenGeneratorHelper;

@Inject
public ConnectionService(ApplicationEventPublisher applicationEventPublisher, ConnectionConfigurationRepository repository, InitializeParams params,
SonarCloudActiveEnvironment sonarCloudActiveEnvironment, TokenGeneratorHelper tokenGeneratorHelper, ServerApiProvider serverApiProvider) {
this(applicationEventPublisher, repository, params.getSonarQubeConnections(), params.getSonarCloudConnections(), sonarCloudActiveEnvironment, serverApiProvider,
SonarCloudActiveEnvironment sonarCloudActiveEnvironment, TokenGeneratorHelper tokenGeneratorHelper, ConnectionManager connectionManager) {
this(applicationEventPublisher, repository, params.getSonarQubeConnections(), params.getSonarCloudConnections(), sonarCloudActiveEnvironment, connectionManager,
tokenGeneratorHelper);
}

ConnectionService(ApplicationEventPublisher applicationEventPublisher, ConnectionConfigurationRepository repository,
@Nullable List<SonarQubeConnectionConfigurationDto> initSonarQubeConnections, @Nullable List<SonarCloudConnectionConfigurationDto> initSonarCloudConnections,
SonarCloudActiveEnvironment sonarCloudActiveEnvironment, ServerApiProvider serverApiProvider, TokenGeneratorHelper tokenGeneratorHelper) {
SonarCloudActiveEnvironment sonarCloudActiveEnvironment, ConnectionManager connectionManager, TokenGeneratorHelper tokenGeneratorHelper) {
this.applicationEventPublisher = applicationEventPublisher;
this.repository = repository;
this.sonarCloudUri = sonarCloudActiveEnvironment.getUri();
this.serverApiProvider = serverApiProvider;
this.connectionManager = connectionManager;
this.tokenGeneratorHelper = tokenGeneratorHelper;
if (initSonarQubeConnections != null) {
initSonarQubeConnections.forEach(c -> repository.addOrReplace(adapt(c)));
Expand Down Expand Up @@ -157,7 +157,7 @@ private void updateConnection(AbstractConnectionConfiguration connectionConfigur

public ValidateConnectionResponse validateConnection(Either<TransientSonarQubeConnectionDto, TransientSonarCloudConnectionDto> transientConnection,
SonarLintCancelMonitor cancelMonitor) {
var serverApi = serverApiProvider.getForTransientConnection(transientConnection);
var serverApi = connectionManager.getForTransientConnection(transientConnection);
var serverChecker = new ServerVersionAndStatusChecker(serverApi);
try {
serverChecker.checkVersionAndStatus(cancelMonitor);
Expand All @@ -179,7 +179,7 @@ public ValidateConnectionResponse validateConnection(Either<TransientSonarQubeCo

public boolean checkSmartNotificationsSupported(Either<TransientSonarQubeConnectionDto, TransientSonarCloudConnectionDto> transientConnection,
SonarLintCancelMonitor cancelMonitor) {
var serverApi = serverApiProvider.getForTransientConnection(transientConnection);
var serverApi = connectionManager.getForTransientConnection(transientConnection);
var developersApi = serverApi.developers();
return developersApi.isSupported(cancelMonitor);
}
Expand All @@ -189,15 +189,15 @@ public HelpGenerateUserTokenResponse helpGenerateUserToken(String serverUrl, Son
}

public List<SonarProjectDto> getAllProjects(Either<TransientSonarQubeConnectionDto, TransientSonarCloudConnectionDto> transientConnection, SonarLintCancelMonitor cancelMonitor) {
var serverApi = serverApiProvider.getForTransientConnection(transientConnection);
var serverApi = connectionManager.getForTransientConnection(transientConnection);
return serverApi.component().getAllProjects(cancelMonitor)
.stream().map(serverProject -> new SonarProjectDto(serverProject.getKey(), serverProject.getName()))
.collect(Collectors.toList());
}

public Map<String, String> getProjectNamesByKey(Either<TransientSonarQubeConnectionDto, TransientSonarCloudConnectionDto> transientConnection,
List<String> projectKeys, SonarLintCancelMonitor cancelMonitor) {
var serverApi = serverApiProvider.getForTransientConnection(transientConnection);
var serverApi = connectionManager.getForTransientConnection(transientConnection);
var projectNamesByKey = new HashMap<String, String>();
projectKeys.forEach(key -> {
var projectName = serverApi.component().getProject(key, cancelMonitor).map(ServerProject::getName).orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@
public class OrganizationsCache {

private static final SonarLintLogger LOG = SonarLintLogger.get();
private final ServerApiProvider serverApiProvider;
private final ConnectionManager connectionManager;

private final Cache<Either<TokenDto, UsernamePasswordDto>, TextSearchIndex<OrganizationDto>> textSearchIndexCacheByCredentials = CacheBuilder.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();

public OrganizationsCache(ServerApiProvider serverApiProvider) {
this.serverApiProvider = serverApiProvider;
public OrganizationsCache(ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}

public List<OrganizationDto> fuzzySearchOrganizations(Either<TokenDto, UsernamePasswordDto> credentials, String searchText, SonarLintCancelMonitor cancelMonitor) {
Expand All @@ -74,7 +74,7 @@ public TextSearchIndex<OrganizationDto> getTextSearchIndex(Either<TokenDto, User
LOG.debug("Load user organizations...");
List<OrganizationDto> orgs;
try {
var serverApi = serverApiProvider.getForSonarCloudNoOrg(credentials);
var serverApi = connectionManager.getForSonarCloudNoOrg(credentials);
var serverOrganizations = serverApi.organization().listUserOrganizations(cancelMonitor);
orgs = serverOrganizations.stream().map(o -> new OrganizationDto(o.getKey(), o.getName(), o.getDescription())).collect(Collectors.toList());
} catch (Exception e) {
Expand Down Expand Up @@ -103,7 +103,7 @@ public List<OrganizationDto> listUserOrganizations(Either<TokenDto, UsernamePass

@CheckForNull
public OrganizationDto getOrganization(Either<TokenDto, UsernamePasswordDto> credentials, String organizationKey, SonarLintCancelMonitor cancelMonitor) {
var helper = serverApiProvider.getForSonarCloudNoOrg(credentials);
var helper = connectionManager.getForSonarCloudNoOrg(credentials);
var serverOrganization = helper.organization().getOrganization(organizationKey, cancelMonitor);
return serverOrganization.map(o -> new OrganizationDto(o.getKey(), o.getName(), o.getDescription())).orElse(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
public class SonarProjectsCache {

private static final SonarLintLogger LOG = SonarLintLogger.get();
private final ServerApiProvider serverApiProvider;
private final ConnectionManager connectionManager;

private final Cache<String, TextSearchIndex<ServerProject>> textSearchIndexCacheByConnectionId = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
Expand Down Expand Up @@ -94,8 +94,8 @@ public int hashCode() {
}
}

public SonarProjectsCache(ServerApiProvider serverApiProvider) {
this.serverApiProvider = serverApiProvider;
public SonarProjectsCache(ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
}

@EventListener
Expand All @@ -120,7 +120,8 @@ public Optional<ServerProject> getSonarProject(String connectionId, String sonar
return singleProjectsCache.get(new SonarProjectKey(connectionId, sonarProjectKey), () -> {
LOG.debug("Query project '{}' on connection '{}'...", sonarProjectKey, connectionId);
try {
return serverApiProvider.getServerApi(connectionId).flatMap(s -> s.component().getProject(sonarProjectKey, cancelMonitor));
return connectionManager.withValidConnectionAndReturn(connectionId,
s -> s.component().getProject(sonarProjectKey, cancelMonitor)).orElse(Optional.empty());
} catch (Exception e) {
LOG.error("Error while querying project '{}' from connection '{}'", sonarProjectKey, connectionId, e);
return Optional.empty();
Expand All @@ -137,7 +138,9 @@ public TextSearchIndex<ServerProject> getTextSearchIndex(String connectionId, So
LOG.debug("Load projects from connection '{}'...", connectionId);
List<ServerProject> projects;
try {
projects = serverApiProvider.getServerApi(connectionId).map(s -> s.component().getAllProjects(cancelMonitor)).orElse(List.of());
projects = connectionManager.withValidConnectionAndReturn(connectionId,
s -> s.component().getAllProjects(cancelMonitor))
.orElse(List.of());
} catch (Exception e) {
LOG.error("Error while querying projects from connection '{}'", connectionId, e);
return new TextSearchIndex<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,17 @@ public class VersionSoonUnsupportedHelper {
private final SonarLintRpcClient client;
private final ConfigurationRepository configRepository;
private final ConnectionConfigurationRepository connectionRepository;
private final ServerApiProvider serverApiProvider;
private final ConnectionManager connectionManager;
private final SynchronizationService synchronizationService;
private final Map<String, Version> cacheConnectionIdPerVersion = new ConcurrentHashMap<>();
private final ExecutorServiceShutdownWatchable<?> executorService;

public VersionSoonUnsupportedHelper(SonarLintRpcClient client, ConfigurationRepository configRepository, ServerApiProvider serverApiProvider,
public VersionSoonUnsupportedHelper(SonarLintRpcClient client, ConfigurationRepository configRepository, ConnectionManager connectionManager,
ConnectionConfigurationRepository connectionRepository, SynchronizationService synchronizationService) {
this.client = client;
this.configRepository = configRepository;
this.connectionRepository = connectionRepository;
this.serverApiProvider = serverApiProvider;
this.connectionManager = connectionManager;
this.synchronizationService = synchronizationService;
this.executorService = new ExecutorServiceShutdownWatchable<>(new ThreadPoolExecutor(0, 1, 10L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(), r -> new Thread(r, "Version Soon Unsupported Helper")));
Expand Down Expand Up @@ -107,23 +107,23 @@ private void queueCheckIfSoonUnsupported(String connectionId, String configScope
try {
var connection = connectionRepository.getConnectionById(connectionId);
if (connection != null && connection.getKind() == ConnectionKind.SONARQUBE) {
var serverApi = serverApiProvider.getServerApiWithoutCredentials(connectionId);
if (serverApi.isPresent()) {
var version = synchronizationService.getServerConnection(connectionId, serverApi.get()).readOrSynchronizeServerVersion(serverApi.get(), cancelMonitor);
var isCached = cacheConnectionIdPerVersion.containsKey(connectionId) && cacheConnectionIdPerVersion.get(connectionId).compareTo(version) == 0;
if (!isCached && VersionUtils.isVersionSupportedDuringGracePeriod(version)) {
client.showSoonUnsupportedMessage(
new ShowSoonUnsupportedMessageParams(
String.format(UNSUPPORTED_NOTIFICATION_ID, connectionId, version.getName()),
configScopeId,
String.format(NOTIFICATION_MESSAGE, version.getName(), connectionId, VersionUtils.getCurrentLts())
)
);
LOG.debug(String.format("Connection '%s' with version '%s' is detected to be soon unsupported",
connection.getConnectionId(), version.getName()));
}
cacheConnectionIdPerVersion.put(connectionId, version);
}
connectionManager.tryGetConnectionWithoutCredentials(connectionId)
.ifPresent(serverConnection -> serverConnection.withClientApi(serverApi -> {
var version = synchronizationService.readOrSynchronizeServerVersion(connectionId, serverApi, cancelMonitor);
var isCached = cacheConnectionIdPerVersion.containsKey(connectionId) && cacheConnectionIdPerVersion.get(connectionId).compareTo(version) == 0;
if (!isCached && VersionUtils.isVersionSupportedDuringGracePeriod(version)) {
client.showSoonUnsupportedMessage(
new ShowSoonUnsupportedMessageParams(
String.format(UNSUPPORTED_NOTIFICATION_ID, connectionId, version.getName()),
configScopeId,
String.format(NOTIFICATION_MESSAGE, version.getName(), connectionId, VersionUtils.getCurrentLts())
)
);
LOG.debug(String.format("Connection '%s' with version '%s' is detected to be soon unsupported",
connection.getConnectionId(), version.getName()));
}
cacheConnectionIdPerVersion.put(connectionId, version);
}));
}
} catch (Exception e) {
LOG.error("Error while checking if soon unsupported", e);
Expand Down
Loading

0 comments on commit faedf81

Please sign in to comment.