Skip to content

Commit

Permalink
fix: token validation & endpoint config for oauth2 [IDE-458] (#549)
Browse files Browse the repository at this point in the history
* feat: diagnostic issue model [IDE-543]

* chore: update protocol version to 14

* chore: refactor code

* chore: remove amplitude and api service

* feat: use sast settings from LS

* wip: ongoing work for scanPublishers

* wip: clean up code

* feat: update the UI with the diagnostics we saved in the cache

* chore: refactor to handle all cache updates in SnykCachedResult

* fix: remove redundant emptyList assignments

* tidy: rename variable to clarify what it represent

* chore: update comments

* fix: update error handling

* fix: snyk code activation/deactivation based on sast from language server

* fix: snyk code activation/deactivation based on sast from language server

* fix: tests

* fix: remove unnecessary files

* docs: add CHANGELOG.md

* fix: put config update last

* fix: token validation

* fix: merge problems

* fix: merge problems

* fix: UI fixes when scanning, sort issue tree

* fix: UI node states

* feat: pipe cli calls to language server

* chore: remove warnings

* fix: adjusted description of an authentication method and removed experimental disclaimer

* fix: ui thread usage & navigation

* fix: super-spawn, simplify ls initialization

* fix: switch default auth to oauth2

* fix: update code action timeout

* fix: mocking in a few iac & oss integ tests

* fix: imports

* fix: rest of the tests in SnykToolWindowPanelIntegTest

* fix: ignore tests

* fix: optimize imports

* fix: language server wrapper tests

* fix: ContainerServiceIntegTest

* fix: OssServiceTest

* fix: optimize imports

---------

Co-authored-by: acke <knut.funkel@snyk.io>
Co-authored-by: Arvyd <arvyd.paeglit@snyk.io>
  • Loading branch information
3 people authored Aug 27, 2024
1 parent 3f86004 commit 8589904
Show file tree
Hide file tree
Showing 30 changed files with 944 additions and 719 deletions.
16 changes: 9 additions & 7 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,8 @@ fun navigateToSource(
virtualFile,
selectionStartOffset,
)
invokeLater {
if (navigatable.canNavigateToSource()) {
if (navigatable.canNavigateToSource()) {
invokeLater {
navigatable.navigate(false)
}
}
Expand Down Expand Up @@ -413,29 +413,31 @@ fun String.toVirtualFile(): VirtualFile {
return if (!this.startsWith("file://")) {
StandardFileSystems.local().refreshAndFindFileByPath(this) ?: throw FileNotFoundException(this)
} else {
VirtualFileManager.getInstance().refreshAndFindFileByUrl(this.toVirtualFileURL()) ?: throw FileNotFoundException(this)
VirtualFileManager.getInstance().refreshAndFindFileByUrl(this.toVirtualFileURL())
?: throw FileNotFoundException(this)
}
}

// add a slash when on windows
fun VirtualFile.toLanguageServerURL(): String {
if (this.urlContainsDriveLetter()) {
return this.url.replace("file://","file:///")
return this.url.replace("file://", "file:///")
}
return this.url
}

// remove first "/" if on windows
fun String.toVirtualFileURL(): String {
if (this.isWindowsURI() && this.startsWith("file:///") && this.length > 10 && this.substring(9,10) == ":") {
return this.replaceFirst("/","")
if (this.isWindowsURI() && this.startsWith("file:///") && this.length > 10 && this.substring(9, 10) == ":") {
return this.replaceFirst("/", "")
}
return this
}

fun String.isWindowsURI() = SystemUtils.IS_OS_WINDOWS && this.startsWith("file://")

fun VirtualFile.urlContainsDriveLetter() = this.url.isWindowsURI() && this.url.length > 9 && this.url.substring(8,9) == ":"
fun VirtualFile.urlContainsDriveLetter() =
this.url.isWindowsURI() && this.url.length > 9 && this.url.substring(8, 9) == ":"

fun VirtualFile.getPsiFile(project: Project): PsiFile? {
return runReadAction { PsiManager.getInstance(project).findFile(this) }
Expand Down
12 changes: 7 additions & 5 deletions src/main/kotlin/io/snyk/plugin/services/CliAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.snyk.plugin.pluginSettings
import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.AUTH_FAILED_TEXT
import org.jetbrains.annotations.TestOnly
import snyk.common.SnykError
import snyk.common.lsp.LanguageServerWrapper
import snyk.errorHandler.SentryErrorReporter

/**
Expand All @@ -35,11 +36,12 @@ abstract class CliAdapter<CliIssues, R : CliResult<CliIssues>>(val project: Proj
*/
fun execute(commands: List<String>): R =
try {
val cmds = buildCliCommandsList(commands)
val apiToken = pluginSettings().token ?: ""
val rawResultStr = consoleCommandRunner.execute(cmds, projectPath, apiToken, project)
val cmds = buildCliCommandsList(commands).toMutableList()
// remove first element = cli path as ls is adding it automatically
cmds.removeAt(0)
val rawResultStr = LanguageServerWrapper.getInstance().executeCLIScan(cmds, projectPath)
convertRawCliStringToCliResult(rawResultStr)
} catch (exception: CliNotExistsException) {
} catch (exception: Exception) {
getErrorResult(exception.message ?: "Snyk CLI not installed.")
}

Expand Down Expand Up @@ -200,6 +202,6 @@ abstract class CliAdapter<CliIssues, R : CliResult<CliIssues>>(val project: Proj
if (isCliInstalled()) getCliFile().absolutePath else throw CliNotExistsException()

companion object {
const val CLI_PRODUCE_NO_OUTPUT = "CLI doesn't produce any output"
const val CLI_PRODUCE_NO_OUTPUT = "CLI didn't produce any output"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ import java.util.UUID
@Service
@State(
name = "SnykApplicationSettingsState",
storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)]
storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)],
)
class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplicationSettingsStateService> {

val requiredLsProtocolVersion = 14

var useTokenAuthentication = false
var currentLSProtocolVersion: Int? = 0

var autofixEnabled: Boolean? = false
var isGlobalIgnoresFeatureEnabled = false
var cliBaseDownloadURL: String = "https://static.snyk.io"
Expand Down Expand Up @@ -100,19 +99,19 @@ class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplica
}
}

fun getAdditionalParameters(project: Project? = null): String? {
return if (isProjectSettingsAvailable(project)) {
fun getAdditionalParameters(project: Project? = null): String? =
if (isProjectSettingsAvailable(project)) {
getSnykProjectSettingsService(project!!)?.additionalParameters
} else {
""
}
}

fun getLastCheckDate(): LocalDate? = if (lastCheckDate != null) {
lastCheckDate!!.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
} else {
null
}
fun getLastCheckDate(): LocalDate? =
if (lastCheckDate != null) {
lastCheckDate!!.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()
} else {
null
}

fun setLastCheckDate(localDate: LocalDateTime) {
this.lastCheckDate = Date.from(localDate.atZone(ZoneId.systemDefault()).toInstant())
Expand All @@ -136,7 +135,10 @@ class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplica
else -> false
}

fun setSeverityTreeFiltered(severity: Severity, state: Boolean) {
fun setSeverityTreeFiltered(
severity: Severity,
state: Boolean,
) {
when (severity) {
Severity.CRITICAL -> treeFiltering.criticalSeverity = state
Severity.HIGH -> treeFiltering.highSeverity = state
Expand All @@ -154,7 +156,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplica
hasSeverityEnabledAndFiltered(Severity.CRITICAL),
hasSeverityEnabledAndFiltered(Severity.HIGH),
hasSeverityEnabledAndFiltered(Severity.MEDIUM),
hasSeverityEnabledAndFiltered(Severity.LOW)
hasSeverityEnabledAndFiltered(Severity.LOW),
).count { it } == 1

fun matchFilteringWithEnablement() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,16 @@ import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.PlatformIcons
import io.snyk.plugin.cli.ConsoleCommandRunner
import io.snyk.plugin.getCliFile
import io.snyk.plugin.getPluginPath
import io.snyk.plugin.getSnykTaskQueueService
import io.snyk.plugin.pluginSettings
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import io.snyk.plugin.ui.getReadOnlyClickableHtmlJEditorPane
import org.apache.commons.text.StringEscapeUtils.escapeHtml4
import snyk.common.getEndpointUrl
import snyk.common.isOauth
import snyk.common.lsp.LanguageServerWrapper
import java.awt.BorderLayout
import java.awt.Dimension
import java.awt.datatransfer.StringSelection
import java.awt.event.ActionEvent
import java.net.URI
import javax.swing.AbstractAction
import javax.swing.Action
import javax.swing.JComponent
Expand All @@ -37,19 +31,17 @@ import javax.swing.JProgressBar
import javax.swing.ScrollPaneConstants

@Service(Service.Level.PROJECT)
class SnykCliAuthenticationService(val project: Project) {
class SnykCliAuthenticationService(
val project: Project,
) {
private val logger = logger<SnykCliAuthenticationService>()

private var isAuthenticated = false
private var token: String = ""

fun authenticate(): String {
token = ""
fun authenticate() {
downloadCliIfNeeded()
if (getCliFile().exists()) executeAuthCommand()
if (isAuthenticated) executeGetConfigApiCommand()
if (getCliFile().exists()) {
executeAuthCommand()
}
LanguageServerWrapper.getInstance().updateConfiguration()
return token
}

private fun downloadCliIfNeeded() {
Expand All @@ -61,7 +53,10 @@ class SnykCliAuthenticationService(val project: Project) {
}
}
ProgressManager.getInstance().runProcessWithProgressSynchronously(
downloadCliTask, "Download Snyk CLI latest release", true, null
downloadCliTask,
"Download Snyk CLI latest release",
true,
null,
)
}

Expand All @@ -70,73 +65,33 @@ class SnykCliAuthenticationService(val project: Project) {

object : Task.Backgroundable(project, "Authenticating Snyk plugin...", true) {
override fun run(indicator: ProgressIndicator) {
dialog.onCancel = { indicator.cancel() }
val endpoint = URI(getEndpointUrl())
var commands = buildCliCommands(listOf("auth"))
if (endpoint.isOauth()) {
commands = buildCliCommands(listOf("auth", "--auth-type=oauth"))
dialog.onCancel = {
indicator.cancel()
}
val finalOutput = getConsoleCommandRunner().execute(commands, getPluginPath(), "", project) { line ->
if (line.startsWith("https://")) {
val htmlLink = escapeHtml4(line.removeLineEnd())
val htmlText =
"""<html>
We are now redirecting you to our auth page, go ahead and log in.<br><br>
Once the authentication is complete, return to the IDE and you'll be ready to start using Snyk.<br><br>
If a browser window doesn't open after a few seconds, please <a href="$htmlLink">click here</a>
or copy the url using the button below and manually paste it in a browser.
</html>
""".trimIndent()
dialog.updateHtmlText(htmlText)
dialog.copyUrlAction.url = htmlLink
dialog.copyUrlAction.isEnabled = true
val languageServerWrapper = LanguageServerWrapper.getInstance()
languageServerWrapper.logout()
val htmlText =
"""
<html>
We are now redirecting you to our auth page, go ahead and log in.<br><br>
Once the authentication is complete, return to the IDE and you'll be ready to start using Snyk.<br><br>
If a browser window doesn't open after a few seconds, please copy the url using the button below and manually paste it in a browser.
</html>
""".trimIndent()
dialog.updateHtmlText(htmlText)
dialog.copyUrlAction.isEnabled = true
languageServerWrapper.login()
val exitCode =
if (!pluginSettings().token.isNullOrBlank()) {
DialogWrapper.OK_EXIT_CODE
} else {
DialogWrapper.CLOSE_EXIT_CODE
}
}
val authSucceed = finalOutput.contains("Your account has been authenticated.")
if (!authSucceed && finalOutput != ConsoleCommandRunner.PROCESS_CANCELLED_BY_USER) {
SnykBalloonNotificationHelper.showError("Failed to authenticate.", project)
}
val exitCode = if (authSucceed) DialogWrapper.OK_EXIT_CODE else DialogWrapper.CLOSE_EXIT_CODE
ApplicationManager.getApplication().invokeLater(
{ dialog.close(exitCode) },
ModalityState.any()
)
ApplicationManager.getApplication().invokeLater({ dialog.close(exitCode) }, ModalityState.any())
}
}.queue()

isAuthenticated = dialog.showAndGet()
}

fun executeGetConfigApiCommand() {
val endpoint = URI(getEndpointUrl())
val getConfigApiTask: () -> Unit = {
var key = "INTERNAL_OAUTH_TOKEN_STORAGE"
if (!endpoint.isOauth()) {
key = "api"
}
val commands = buildCliCommands(listOf("config", "get", key))
val getConfigApiOutput = getConsoleCommandRunner().execute(commands, getPluginPath(), "", project)
token = getConfigApiOutput.removeLineEnd()
}
ProgressManager.getInstance().runProcessWithProgressSynchronously(
getConfigApiTask, "Get Snyk API Token", true, null
)
}

private fun buildCliCommands(commands: List<String>): List<String> {
val settings = pluginSettings()
val cli: MutableList<String> = mutableListOf(getCliFile().absolutePath)
cli.addAll(commands)

if (settings.ignoreUnknownCA) {
cli.add("--insecure")
}

return cli.toList()
}

private fun getConsoleCommandRunner(): ConsoleCommandRunner {
return ConsoleCommandRunner()
dialog.showAndGet()
}
}

Expand All @@ -153,16 +108,18 @@ class AuthDialog : DialogWrapper(true) {

override fun createCenterPanel(): JComponent {
val centerPanel = JPanel(BorderLayout(JBUIScale.scale(5), JBUIScale.scale(5)))
val scrollPane = JBScrollPane(
viewer,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
)
val scrollPane =
JBScrollPane(
viewer,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER,
)
centerPanel.add(scrollPane, BorderLayout.CENTER)

val progressBar = JProgressBar().apply {
isIndeterminate = true
}
val progressBar =
JProgressBar().apply {
isIndeterminate = true
}
centerPanel.add(progressBar, BorderLayout.SOUTH)

centerPanel.preferredSize = Dimension(500, 150)
Expand All @@ -183,14 +140,17 @@ class AuthDialog : DialogWrapper(true) {

override fun createLeftSideActions(): Array<Action> = arrayOf(copyUrlAction)

inner class CopyUrlAction(var url: String = "") : AbstractAction("&Copy URL", PlatformIcons.COPY_ICON) {
inner class CopyUrlAction : AbstractAction("&Copy URL", PlatformIcons.COPY_ICON) {
override fun actionPerformed(e: ActionEvent) {
CopyPasteManager.getInstance().setContents(StringSelection(url))
SnykBalloonNotificationHelper.showInfoBalloonForComponent(
"URL copied",
getButton(this) ?: viewer,
showAbove = getButton(this) != null
)
val url = LanguageServerWrapper.getInstance().getAuthLink()
if (url != null) {
CopyPasteManager.getInstance().setContents(StringSelection(url))
SnykBalloonNotificationHelper.showInfoBalloonForComponent(
"URL copied",
getButton(this) ?: viewer,
showAbove = getButton(this) != null,
)
}
}
}
}
Expand Down
Loading

0 comments on commit 8589904

Please sign in to comment.