Skip to content

Commit

Permalink
SLI-378 Prefer PasswordSafe to PasswordUtil
Browse files Browse the repository at this point in the history
  • Loading branch information
damien-urruty-sonarsource committed Oct 30, 2023
1 parent 1f86ed2 commit 064e677
Show file tree
Hide file tree
Showing 67 changed files with 1,440 additions and 1,318 deletions.
3 changes: 3 additions & 0 deletions .cirrus/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ FROM ${CIRRUS_AWS_ACCOUNT}.dkr.ecr.eu-central-1.amazonaws.com/base:j${JDK_VERSIO
USER root

ENV NODE_VERSION=18

# dbus-x11 is for SonarLint token secure storage (required by the IDE)
RUN apt-get update && apt-get install -y metacity xvfb xauth ffmpeg \
dbus-x11 \
nodejs=${NODE_VERSION}.* \
build-essential \
gettext-base \
Expand Down
15 changes: 8 additions & 7 deletions src/main/java/org/sonarlint/intellij/SonarLintIntelliJClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ import org.sonarlint.intellij.analysis.AnalysisSubmitter
import org.sonarlint.intellij.common.ui.ReadActionUtils.Companion.computeReadActionSafely
import org.sonarlint.intellij.common.util.SonarLintUtils
import org.sonarlint.intellij.common.util.SonarLintUtils.getService
import org.sonarlint.intellij.config.Settings.getGlobalSettings
import org.sonarlint.intellij.config.Settings.getSettingsFor
import org.sonarlint.intellij.config.global.ServerConnectionService
import org.sonarlint.intellij.config.global.wizard.ServerConnectionCreator
import org.sonarlint.intellij.core.BackendService
import org.sonarlint.intellij.core.ProjectBindingManager
Expand Down Expand Up @@ -334,7 +334,7 @@ object SonarLintIntelliJClient : SonarLintClient {
return CompletableFuture.supplyAsync {
val connectionId = params.connectionId
val projectKey = params.projectKey
val connection = getGlobalSettings().getServerConnectionByName(connectionId)
val connection = ServerConnectionService.getInstance().getServerConnectionByName(connectionId)
.orElseThrow { IllegalStateException("Unable to find connection '$connectionId'") }
val message = "Cannot automatically find a project bound to:\n" +
" • Project: $projectKey\n" +
Expand Down Expand Up @@ -390,11 +390,12 @@ object SonarLintIntelliJClient : SonarLintClient {
}

override fun getCredentials(params: GetCredentialsParams): CompletableFuture<GetCredentialsResponse> {
return getGlobalSettings().getServerConnectionByName(params.connectionId)
.map { connection -> connection.token?.let { CompletableFuture.completedFuture(GetCredentialsResponse(TokenDto(it))) }
?: connection.login?.let { CompletableFuture.completedFuture(GetCredentialsResponse(UsernamePasswordDto(it, connection.password))) }
?: CompletableFuture.failedFuture(IllegalArgumentException("Invalid credentials for connection: " + params.connectionId))
}.orElse(CompletableFuture.failedFuture(IllegalArgumentException("Unknown connection: " + params.connectionId)))
val connectionId = params.connectionId
return ServerConnectionService.getInstance().getServerConnectionByName(connectionId)
.map { connection -> connection.credentials.token?.let { CompletableFuture.completedFuture(GetCredentialsResponse(TokenDto(it))) }
?: connection.credentials.login?.let { CompletableFuture.completedFuture(GetCredentialsResponse(UsernamePasswordDto(it, connection.credentials.password))) }
?: CompletableFuture.failedFuture(IllegalArgumentException("Invalid credentials for connection: $connectionId"))}
.orElseGet { CompletableFuture.failedFuture(IllegalArgumentException("Connection '$connectionId' not found")) }
}

override fun getProxyPasswordAuthentication(params: GetProxyPasswordAuthenticationParams): CompletableFuture<GetProxyPasswordAuthenticationResponse> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class OpenIssueInBrowserAction : AbstractSonarAction(
override fun updatePresentation(e: AnActionEvent, project: Project) {
val serverConnection = serverConnection(project) ?: return
e.presentation.text = "Open in " + serverConnection.productName
e.presentation.icon = serverConnection.productIcon
e.presentation.icon = serverConnection.product.icon
}

override fun actionPerformed(e: AnActionEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class OpenSecurityHotspotInBrowserAction : AbstractSonarAction(
val serverConnection = serverConnection(project) ?: return
e.presentation.text = "Open in " + serverConnection.productName
e.presentation.description = "Open Security Hotspot in browser interface of " + serverConnection.productName
e.presentation.icon = serverConnection.productIcon
e.presentation.icon = serverConnection.product.icon
}

override fun actionPerformed(e: AnActionEvent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* SonarLint for IntelliJ IDEA
* Copyright (C) 2015-2023 SonarSource
* [email protected]
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonarlint.intellij.config.global

import org.sonarlint.intellij.common.util.SonarLintUtils
import org.sonarlint.intellij.common.util.SonarLintUtils.SONARCLOUD_URL
import org.sonarlint.intellij.core.BackendService
import org.sonarlint.intellij.core.SonarProduct
import org.sonarlint.intellij.core.server.ServerLinks
import org.sonarlint.intellij.core.server.SonarCloudLinks
import org.sonarlint.intellij.core.server.SonarQubeLinks
import org.sonarsource.sonarlint.core.serverapi.EndpointParams
import org.sonarsource.sonarlint.core.serverapi.ServerApi


sealed class ServerConnection {
abstract val name: String
abstract val notificationsDisabled: Boolean
abstract val hostUrl: String
abstract val credentials: ServerConnectionCredentials
abstract val product: SonarProduct
abstract val links: ServerLinks
abstract val endpointParams: EndpointParams
fun api() = ServerApi(endpointParams, SonarLintUtils.getService(BackendService::class.java).getHttpClient(name))
override fun toString() = name
val isSonarCloud get() = product == SonarProduct.SONARCLOUD
val isSonarQube get() = product == SonarProduct.SONARQUBE
val productName get() = product.productName
}

data class SonarQubeConnection(override val name: String, override val hostUrl: String, override val credentials: ServerConnectionCredentials, override val notificationsDisabled: Boolean) : ServerConnection() {
override val product = SonarProduct.SONARQUBE
override val links = SonarQubeLinks(hostUrl)
override val endpointParams = EndpointParams(hostUrl, false, null)
}

data class SonarCloudConnection(override val name: String, val token: String, val organizationKey: String, override val notificationsDisabled: Boolean) : ServerConnection() {
override val credentials = ServerConnectionCredentials(null, null, token)
override val product = SonarProduct.SONARCLOUD
override val links = SonarCloudLinks
override val hostUrl: String = SONARCLOUD_URL
override val endpointParams = EndpointParams(hostUrl, true, organizationKey)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* SonarLint for IntelliJ IDEA
* Copyright (C) 2015-2023 SonarSource
* [email protected]
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonarlint.intellij.config.global

data class ServerConnectionCredentials(val login: String?, val password: String?, val token: String?)
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
import com.intellij.ui.ToolbarDecorator;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBList;
import org.sonarlint.intellij.SonarLintIcons;
import java.awt.BorderLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
Expand Down Expand Up @@ -105,15 +104,11 @@ public void mouseClicked(MouseEvent evt) {

connectionList.setCellRenderer(new ColoredListCellRenderer<>() {
@Override
protected void customizeCellRenderer(JList list, ServerConnection server, int index, boolean selected, boolean hasFocus) {
if (server.isSonarCloud()) {
setIcon(SonarLintIcons.ICON_SONARCLOUD_16);
} else {
setIcon(SonarLintIcons.ICON_SONARQUBE_16);
}
append(server.getName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
if (!server.isSonarCloud()) {
append(" (" + server.getHostUrl() + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
protected void customizeCellRenderer(JList list, ServerConnection connection, int index, boolean selected, boolean hasFocus) {
setIcon(connection.getProduct().getIcon());
append(connection.getName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
if (connection.isSonarQube()) {
append(" (" + connection.getHostUrl() + ")", SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
}
}
});
Expand Down Expand Up @@ -144,14 +139,12 @@ public JComponent getComponent() {

@Override
public boolean isModified(SonarLintGlobalSettings settings) {
return !connections.equals(settings.getServerConnections());
return !connections.equals(ServerConnectionService.getInstance().getConnections());
}

@Override
public void save(SonarLintGlobalSettings newSettings) {
var newConnections = new ArrayList<>(connections);
newSettings.setServerConnections(newConnections);

ServerConnectionService.getInstance().setServerConnections(newSettings, connections);
// remove them even if a server with the same name was later added
unbindRemovedServers();
}
Expand All @@ -162,8 +155,9 @@ public void load(SonarLintGlobalSettings settings) {
deletedServerIds.clear();

var listModel = new CollectionListModel<ServerConnection>(new ArrayList<>());
listModel.add(settings.getServerConnections());
connections.addAll(settings.getServerConnections());
var serverConnections = ServerConnectionService.getInstance().getConnections();
listModel.add(serverConnections);
connections.addAll(serverConnections);
connectionList.setModel(listModel);

if (!connections.isEmpty()) {
Expand Down Expand Up @@ -213,15 +207,15 @@ public void run(AnActionButton anActionButton) {
private class RemoveServerAction implements AnActionButtonRunnable {
@Override
public void run(AnActionButton anActionButton) {
var server = getSelectedConnection();
var connection = getSelectedConnection();
var selectedIndex = connectionList.getSelectedIndex();

if (server == null) {
if (connection == null) {
return;
}

var openProjects = ProjectManager.getInstance().getOpenProjects();
var projectsUsingNames = getOpenProjectNames(openProjects, server);
var projectsUsingNames = getOpenProjectNames(openProjects, connection);

if (!projectsUsingNames.isEmpty()) {
var projects = String.join("<br>", projectsUsingNames);
Expand All @@ -236,8 +230,8 @@ public void run(AnActionButton anActionButton) {

var model = (CollectionListModel<ServerConnection>) connectionList.getModel();
// it's not removed from serverIds and editorList
model.remove(server);
connections.remove(server);
model.remove(connection);
connections.remove(connection);
connectionChangeListener.changed(connections);

if (model.getSize() > 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* SonarLint for IntelliJ IDEA
* Copyright (C) 2015-2023 SonarSource
* [email protected]
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
*/
package org.sonarlint.intellij.config.global

import com.intellij.credentialStore.CredentialAttributes
import com.intellij.credentialStore.Credentials
import com.intellij.credentialStore.generateServiceName
import com.intellij.ide.passwordSafe.PasswordSafe
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import java.util.Optional
import kotlinx.collections.immutable.toImmutableList
import org.sonarlint.intellij.common.util.SonarLintUtils.getService
import org.sonarlint.intellij.config.Settings.getGlobalSettings
import org.sonarlint.intellij.messages.ServerConnectionsListener
import org.sonarlint.intellij.util.GlobalLogOutput

@Service(Service.Level.APP)
class ServerConnectionService {

init {
loadAndMigrateServerConnections()
}

private fun loadAndMigrateServerConnections() {
getGlobalSettings().serverConnections.forEach { migrate(it) }
}

private fun migrate(connection: ServerConnectionSettings) {
saveCredentials(connection.name, ServerConnectionCredentials(connection.login, connection.password, connection.token))
connection.clearCredentials()
}

fun getConnections(): List<ServerConnection> = getGlobalSettings().serverConnections.filter { isValid(it) }.mapNotNull { connection ->
val credentials = loadCredentials(connection.name) ?: return@mapNotNull null
if (connection.isSonarCloud) {
val token = credentials.token ?: run {
GlobalLogOutput.get().logError("Token not found in secure storage for connection $connection", null)
return@mapNotNull null
}
SonarCloudConnection(connection.name, token, connection.organizationKey!!, connection.isDisableNotifications)
} else SonarQubeConnection(connection.name, connection.hostUrl, credentials, connection.isDisableNotifications)
}

private fun isValid(connectionSettings: ServerConnectionSettings): Boolean {
val valid = connectionSettings.name != null && if (connectionSettings.isSonarCloud) connectionSettings.organizationKey != null
else connectionSettings.hostUrl != null
if (!valid) {
GlobalLogOutput.get().logError("The connection $connectionSettings is not valid", null)
}
return valid
}

fun getServerConnectionByName(name: String): Optional<ServerConnection> {
return Optional.ofNullable(getConnections().firstOrNull { name == it.name })
}

fun connectionExists(connectionName: String): Boolean {
return getConnections().any { it.name == connectionName }
}

fun getServerNames(): Set<String> {
return getConnections().map { it.name }.toSet()
}

fun addServerConnection(connection: ServerConnection) {
setServerConnections(getConnections() + connection)
}

fun replaceConnection(name: String, replacementConnection: ServerConnection) {
val serverConnections = getConnections().toMutableList()
serverConnections[serverConnections.indexOfFirst { it.name == name }] = replacementConnection
setServerConnections(serverConnections.toImmutableList())
}

private fun setServerConnections(connections: List<ServerConnection>) {
setServerConnections(getGlobalSettings(), connections)
}

fun setServerConnections(settings: SonarLintGlobalSettings, connections: List<ServerConnection>) {
val previousConnections = getConnections()
settings.serverConnections = connections.map {
var builder = ServerConnectionSettings.newBuilder().setName(it.name).setHostUrl(it.hostUrl).setDisableNotifications(it.notificationsDisabled)
if (it is SonarCloudConnection) {
builder = builder.setOrganizationKey(it.organizationKey)
}
builder.build()
}.toList()
connections.forEach { saveCredentials(it.name, it.credentials) }
notifyConnectionsChange(connections)
notifyCredentialsChange(previousConnections, connections)
val removedConnectionNames = previousConnections.map { it.name }.filter { name -> !connections.map { it.name }.contains(name) }
removedConnectionNames.forEach { forgetCredentials(it) }
}

private fun notifyConnectionsChange(connections: List<ServerConnection>) {
ApplicationManager.getApplication().messageBus.syncPublisher(ServerConnectionsListener.TOPIC).afterChange(connections)
}

private fun notifyCredentialsChange(previousConnections: List<ServerConnection>, newConnections: List<ServerConnection>) {
val changedConnections = newConnections.filter { connection ->
val previousConnection = previousConnections.find { it.name == connection.name }
previousConnection?.let { connection.credentials != it.credentials } == true
}
if (changedConnections.isNotEmpty()) {
ApplicationManager.getApplication().messageBus.syncPublisher(ServerConnectionsListener.TOPIC).credentialsChanged(changedConnections)
}
}

private fun loadCredentials(connectionId: String): ServerConnectionCredentials? {
val token = PasswordSafe.instance.getPassword(tokenCredentials(connectionId))
if (token != null) {
return ServerConnectionCredentials(null, null, token)
}
val loginPassword = PasswordSafe.instance[loginPasswordCredentials(connectionId)]
if (loginPassword != null) {
return ServerConnectionCredentials(loginPassword.userName, loginPassword.password.toString(), null)
}
GlobalLogOutput.get().logError("Unable to retrieve credentials from secure storage for connection '$connectionId'", null)
return null
}

private fun saveCredentials(connectionId: String, credentials: ServerConnectionCredentials) {
if (credentials.token != null) {
PasswordSafe.instance.setPassword(tokenCredentials(connectionId), credentials.token)
} else if (credentials.login != null && credentials.password != null) {
PasswordSafe.instance[loginPasswordCredentials(connectionId)] = Credentials(credentials.login, credentials.password)
}
// else probably already migrated
}

private fun forgetCredentials(connectionId: String) {
PasswordSafe.instance[tokenCredentials(connectionId)] = null
}

companion object {
@JvmStatic
fun getInstance(): ServerConnectionService = getService(ServerConnectionService::class.java)
private fun tokenCredentials(connectionId: String) = CredentialAttributes(generateServiceName("SonarLint connections", "$connectionId.token"))
private fun loginPasswordCredentials(connectionId: String) = CredentialAttributes(generateServiceName("SonarLint connections", "$connectionId.loginPassword"))
}
}
Loading

0 comments on commit 064e677

Please sign in to comment.