From 630ad8cf11cced81bc2d9a18018e46082cc866c9 Mon Sep 17 00:00:00 2001 From: JSON Date: Fri, 14 Apr 2023 10:44:46 +0100 Subject: [PATCH] feat: support oauth authentication flow (#132) Co-authored-by: Bastian Doetsch --- .../eclipse/plugin/EnvironmentConstants.java | 2 + .../io/snyk/eclipse/plugin/SnykStartup.java | 39 ++--- .../properties/preferences/ApiClient.java | 11 +- .../properties/preferences/Preferences.java | 9 +- .../eclipse/plugin/runner/AuthResponse.java | 53 ------ .../eclipse/plugin/runner/Authenticator.java | 161 ------------------ .../eclipse/plugin/runner/ProcessRunner.java | 17 +- .../languageserver/SnykLanguageServer.java | 24 +-- .../SnykExtendedLanguageClient.java | 52 +++++- .../messageObjects/OAuthToken.java | 76 +++++++++ .../preferences/PreferencesTest.java | 7 +- .../plugin/runner/ProcessRunnerTest.java | 27 ++- .../SnykExtendedLanguageClientTest.java | 53 +++++- 13 files changed, 255 insertions(+), 276 deletions(-) delete mode 100644 plugin/src/main/java/io/snyk/eclipse/plugin/runner/AuthResponse.java delete mode 100644 plugin/src/main/java/io/snyk/eclipse/plugin/runner/Authenticator.java create mode 100644 plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/OAuthToken.java diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/EnvironmentConstants.java b/plugin/src/main/java/io/snyk/eclipse/plugin/EnvironmentConstants.java index da1cb763..f5763d82 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/EnvironmentConstants.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/EnvironmentConstants.java @@ -5,4 +5,6 @@ public interface EnvironmentConstants { String ENV_SNYK_TOKEN = "SNYK_TOKEN"; String ENV_SNYK_ORG = "SNYK_CFG_ORG"; String ENV_DISABLE_ANALYTICS = "SNYK_CFG_DISABLE_ANALYTICS"; + String ENV_INTERNAL_SNYK_OAUTH_ENABLED = "INTERNAL_SNYK_OAUTH_ENABLED"; + String ENV_INTERNAL_OAUTH_TOKEN_STORAGE = "INTERNAL_OAUTH_TOKEN_STORAGE"; } diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/SnykStartup.java b/plugin/src/main/java/io/snyk/eclipse/plugin/SnykStartup.java index f26b95f1..1cbad3e5 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/SnykStartup.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/SnykStartup.java @@ -1,18 +1,15 @@ package io.snyk.eclipse.plugin; -import io.snyk.eclipse.plugin.properties.preferences.Preferences; -import io.snyk.eclipse.plugin.utils.SnykLogger; -import io.snyk.eclipse.plugin.views.SnykView; -import io.snyk.eclipse.plugin.wizards.SnykWizard; -import io.snyk.languageserver.LsRuntimeEnvironment; -import io.snyk.languageserver.SnykLanguageServer; -import io.snyk.languageserver.download.HttpClientFactory; -import io.snyk.languageserver.download.LsBinaries; -import io.snyk.languageserver.download.LsDownloader; +import static io.snyk.eclipse.plugin.utils.SnykLogger.logError; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; +import java.time.Instant; +import java.time.temporal.ChronoUnit; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; -import org.eclipse.core.net.proxy.IProxyData; import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; @@ -26,15 +23,15 @@ import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.attribute.BasicFileAttributes; -import java.time.Instant; -import java.time.temporal.ChronoUnit; - -import static io.snyk.eclipse.plugin.utils.SnykLogger.logError; +import io.snyk.eclipse.plugin.properties.preferences.Preferences; +import io.snyk.eclipse.plugin.utils.SnykLogger; +import io.snyk.eclipse.plugin.views.SnykView; +import io.snyk.eclipse.plugin.wizards.SnykWizard; +import io.snyk.languageserver.LsRuntimeEnvironment; +import io.snyk.languageserver.SnykLanguageServer; +import io.snyk.languageserver.download.HttpClientFactory; +import io.snyk.languageserver.download.LsBinaries; +import io.snyk.languageserver.download.LsDownloader; public class SnykStartup implements IStartup { private LsRuntimeEnvironment runtimeEnvironment; diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/ApiClient.java b/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/ApiClient.java index 1660ff40..2052c77f 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/ApiClient.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/ApiClient.java @@ -5,6 +5,8 @@ import io.snyk.languageserver.LsRuntimeEnvironment; import io.snyk.languageserver.download.HttpClientFactory; +import io.snyk.languageserver.protocolextension.SnykExtendedLanguageClient; +import io.snyk.languageserver.protocolextension.messageObjects.OAuthToken; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.protocol.HttpClientContext; @@ -42,7 +44,14 @@ public boolean checkSnykCodeEnablement() { url += "?org=" + org; } var httpGet = new HttpGet(endpoint + url); - httpGet.addHeader("Authorization", "token " + prefs.getAuthToken()); + if (prefs.getPref(Preferences.AUTHENTICATION_METHOD).equals(Preferences.AUTH_METHOD_TOKEN)) { + httpGet.addHeader("Authorization", "token " + prefs.getAuthToken()); + } else { + // first refresh token + SnykExtendedLanguageClient.getInstance().refreshOAuthToken(); + var oauthToken = objectMapper.readValue(prefs.getAuthToken(), OAuthToken.class); + httpGet.addHeader("Authorization", "bearer " + oauthToken.getAccessToken()); + } httpGet.addHeader("Content-Type", "application/json"); var response = httpClient.execute(httpGet, context); diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/Preferences.java b/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/Preferences.java index 43cc0c4f..bcb00450 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/Preferences.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/properties/preferences/Preferences.java @@ -46,7 +46,9 @@ public static synchronized Preferences getInstance(PreferenceStore store) { public static final String ENABLE_TELEMETRY = EnvironmentConstants.ENV_DISABLE_ANALYTICS; public static final String MANAGE_BINARIES_AUTOMATICALLY = "SNYK_CFG_MANAGE_BINARIES_AUTOMATICALLY"; public static final String ORGANIZATION_KEY = EnvironmentConstants.ENV_SNYK_ORG; - + public static final String AUTHENTICATION_METHOD = "AUTHENTICATION_METHOD"; + public static final String AUTH_METHOD_TOKEN = "token"; + public static final String AUTH_METHOD_OAUTH = "oauth"; private final PreferenceStore store; @@ -76,6 +78,11 @@ public static synchronized Preferences getInstance(PreferenceStore store) { if (getPref(LSP_VERSION) == null) { store(LSP_VERSION, "1"); } + + if (getPref(AUTHENTICATION_METHOD) == null || getPref(AUTHENTICATION_METHOD).isBlank()) { + store(AUTHENTICATION_METHOD, AUTH_METHOD_TOKEN); + } + if (getPref(LS_BINARY_KEY) == null || getPref(LS_BINARY_KEY).equals("")) { store(LS_BINARY_KEY, getDefaultLsPath()); } diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/runner/AuthResponse.java b/plugin/src/main/java/io/snyk/eclipse/plugin/runner/AuthResponse.java deleted file mode 100644 index 383f9060..00000000 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/runner/AuthResponse.java +++ /dev/null @@ -1,53 +0,0 @@ -package io.snyk.eclipse.plugin.runner; - -public class AuthResponse { - boolean ok; - String api; - - public AuthResponse() { - } - - public boolean isOk() { - return this.ok; - } - - public String getApi() { - return this.api; - } - - public void setOk(boolean ok) { - this.ok = ok; - } - - public void setApi(String api) { - this.api = api; - } - - public boolean equals(final Object o) { - if (o == this) return true; - if (!(o instanceof AuthResponse)) return false; - final AuthResponse other = (AuthResponse) o; - if (!other.canEqual(this)) return false; - if (this.isOk() != other.isOk()) return false; - final Object this$api = this.getApi(); - final Object other$api = other.getApi(); - return this$api == null ? other$api == null : this$api.equals(other$api); - } - - protected boolean canEqual(final Object other) { - return other instanceof AuthResponse; - } - - public int hashCode() { - final int PRIME = 59; - int result = 1; - result = result * PRIME + (this.isOk() ? 79 : 97); - final Object $api = this.getApi(); - result = result * PRIME + ($api == null ? 43 : $api.hashCode()); - return result; - } - - public String toString() { - return "AuthResponse(ok=" + this.isOk() + ", api=" + this.getApi() + ")"; - } -} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/runner/Authenticator.java b/plugin/src/main/java/io/snyk/eclipse/plugin/runner/Authenticator.java deleted file mode 100644 index 1447756c..00000000 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/runner/Authenticator.java +++ /dev/null @@ -1,161 +0,0 @@ -package io.snyk.eclipse.plugin.runner; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.snyk.eclipse.plugin.exception.AuthException; -import io.snyk.eclipse.plugin.properties.preferences.Preferences; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLContextBuilder; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.util.EntityUtils; -import org.eclipse.ui.PlatformUI; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLContext; -import java.io.IOException; -import java.net.URL; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.util.UUID; - -import static io.snyk.eclipse.plugin.utils.MockHandler.MOCK; - -public class Authenticator { - - public static final Authenticator INSTANCE = new Authenticator(); - - private static final String API_URL = "https://snyk.io"; - - private final SnykCliRunner cliRunner = new SnykCliRunner(); - ObjectMapper objectMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - private boolean isAuthenticated() throws AuthException { - if (MOCK) - return true; - - ProcessResult procesResult = cliRunner.snykConfig(); - if (procesResult.hasError()) { - throw new AuthException(procesResult.getError()); - } - - if (procesResult.hasContentError()) { - throw new AuthException(procesResult.getContent()); - } - - String content = procesResult.getContent(); - String authToken = Preferences.getInstance().getAuthToken(); - if (content != null && authToken != null) { - return content.contains(authToken); - } - return false; - } - - private void auth() throws AuthException { - // don't authenticate using 'snyk auth' -// ProcessResult procesResult = cliRunner.snykAuth(); -// if (procesResult.hasError()) { -// throw new AuthException(procesResult.getError()); -// } -// -// String content = procesResult.getContent(); -// if (content != null && content.contains("failed")) { -// throw new AuthException(procesResult.getContent()); -// } - } - - private void doAuthentication() throws AuthException { - if (!isAuthenticated()) - auth(); - } - - // call login url and do callback - private String callLogin() throws AuthException { - String newToken = UUID.randomUUID().toString(); - - String loginUri = getAuthUrlBase() + "/login?token=" + newToken + - "&from=eclipsePlugin"; - - try { - PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(loginUri)); - Thread.sleep(2000); - return pollCallback(newToken); - } catch (Exception e) { - throw new AuthException("Authentication problem, " + e.getMessage(), e); - } - - } - - private String pollCallback(String token) throws IOException, InterruptedException, AuthException, - KeyManagementException, NoSuchAlgorithmException, KeyStoreException { - - //Only if insecure flag is active, create httpclient that accepts all ssl cert - HttpClient httpClient = Preferences.getInstance().isInsecure() ? httpClientIgnoresCerts() : HttpClientBuilder.create().build(); - String payload = "{\"token\" : \"" + token + "\"}"; - - StringEntity payloadEntity = new StringEntity(payload); - - HttpPost post = new HttpPost(getAuthUrlBase() + "/api/verify/callback"); - post.setHeader("Content-type", "application/json"); - post.setHeader("user-agent", "Needle/2.1.1 (Node.js v8.11.3; linux x64)"); - post.setEntity(payloadEntity); - - for (int i = 0; i < 20; i++) { - HttpResponse response = httpClient.execute(post); - String responseJson = EntityUtils.toString(response.getEntity(), "UTF-8"); - AuthResponse authResponse = objectMapper.readValue(responseJson, AuthResponse.class); - if (authResponse.isOk()) { - return authResponse.getApi(); - } - Thread.sleep(2000); - } - - throw new AuthException("timeout, please try again"); - - } - - @SuppressWarnings("deprecation") - private HttpClient httpClientIgnoresCerts() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { - HttpClientBuilder b = HttpClientBuilder.create(); - - SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (arg0, arg1) -> true).build(); - b.setSslcontext(sslContext); - - HostnameVerifier hostnameVerifier = SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; - - SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, hostnameVerifier); - Registry socketFactoryRegistry = RegistryBuilder.create() - .register("http", PlainConnectionSocketFactory.getSocketFactory()) - .register("https", sslSocketFactory) - .build(); - - PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(socketFactoryRegistry); - b.setConnectionManager(connMgr); - - return b.build(); - } - - private String getAuthUrlBase() throws AuthException { - String customEndpoint = Preferences.getInstance().getEndpoint(); - if (customEndpoint == null || customEndpoint.isEmpty()) { - return API_URL; - } - - try { - URL endpoint = new URL(Preferences.getInstance().getEndpoint()); - return endpoint.getProtocol() + "://" + endpoint.getAuthority(); - } catch (Exception e) { - throw new AuthException("Authentication problem, " + e.getMessage(), e); - } - } - -} diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/runner/ProcessRunner.java b/plugin/src/main/java/io/snyk/eclipse/plugin/runner/ProcessRunner.java index 453ea900..fdda9f3a 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/runner/ProcessRunner.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/runner/ProcessRunner.java @@ -31,7 +31,7 @@ public class ProcessRunner { private static final String HOME = System.getProperty("user.home"); private static final String DEFAULT_MAC_PATH = "/usr/local/bin:/usr/bin:/bin:/sbin:/usr/sbin:" + HOME + "/bin:" - + HOME + "/.cargo/bin:" + System.getenv("GOPATH") + "/bin" + System.getenv("GOROOT") + "/bin"; + + HOME + "/.cargo/bin:" + System.getenv("GOPATH") + "/bin" + System.getenv("GOROOT") + "/bin"; private static final String DEFAULT_LINUX_PATH = DEFAULT_MAC_PATH; private static final String DEFAULT_WIN_PATH = ""; @@ -89,8 +89,8 @@ private ProcessBuilder getProcessBuilder(List params, Optional p // TODO: move to runtimeEnvironment if (path.isPresent() && !path.get().isBlank()) { pb.environment().put("PATH", - path.map(p -> p + File.pathSeparator + defaultPathForOS).orElse(defaultPathForOS) - + File.pathSeparator + System.getenv("PATH")); + path.map(p -> p + File.pathSeparator + defaultPathForOS).orElse(defaultPathForOS) + + File.pathSeparator + System.getenv("PATH")); } return pb; } @@ -120,9 +120,14 @@ private void setupProcessBuilderBase(ProcessBuilder pb) { } } + String authMethod = Preferences.getInstance().getPref(Preferences.AUTHENTICATION_METHOD); String token = Preferences.getInstance().getAuthToken(); - if (token != null) - pb.environment().put(EnvironmentConstants.ENV_SNYK_TOKEN, Preferences.getInstance().getAuthToken()); + if (token != null && authMethod.equals(Preferences.AUTH_METHOD_OAUTH)) { + pb.environment().put(EnvironmentConstants.ENV_INTERNAL_SNYK_OAUTH_ENABLED, "1"); + pb.environment().put(EnvironmentConstants.ENV_INTERNAL_OAUTH_TOKEN_STORAGE, token); + } else { + pb.environment().put(EnvironmentConstants.ENV_SNYK_TOKEN, token); + } String insecure = Preferences.getInstance().getPref(Preferences.INSECURE_KEY); if (insecure != null && insecure.equalsIgnoreCase("true")) @@ -150,7 +155,7 @@ public ProcessBuilder createWinProcessBuilder(List params, Optional p + ";" + DEFAULT_WIN_PATH).orElse(DEFAULT_WIN_PATH) - + File.pathSeparator + System.getenv("PATH")); + + File.pathSeparator + System.getenv("PATH")); // debug logging on windows machines IStatus[] statuses = new IStatus[] { diff --git a/plugin/src/main/java/io/snyk/languageserver/SnykLanguageServer.java b/plugin/src/main/java/io/snyk/languageserver/SnykLanguageServer.java index 6f564500..788be19e 100644 --- a/plugin/src/main/java/io/snyk/languageserver/SnykLanguageServer.java +++ b/plugin/src/main/java/io/snyk/languageserver/SnykLanguageServer.java @@ -1,9 +1,10 @@ package io.snyk.languageserver; -import io.snyk.eclipse.plugin.SnykStartup; -import io.snyk.eclipse.plugin.properties.preferences.Preferences; -import io.snyk.eclipse.plugin.utils.Lists; -import io.snyk.eclipse.plugin.utils.SnykLogger; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.List; + import org.apache.commons.lang3.SystemUtils; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; @@ -11,22 +12,15 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.lsp4e.LSPEclipseUtils; -import org.eclipse.lsp4e.LanguageServerWrapper; import org.eclipse.lsp4e.LanguageServersRegistry; import org.eclipse.lsp4e.LanguageServiceAccessor; import org.eclipse.lsp4e.server.ProcessStreamConnectionProvider; import org.eclipse.lsp4e.server.StreamConnectionProvider; -import org.eclipse.lsp4j.services.LanguageServer; -import org.eclipse.ui.PlatformUI; -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; +import io.snyk.eclipse.plugin.SnykStartup; +import io.snyk.eclipse.plugin.properties.preferences.Preferences; +import io.snyk.eclipse.plugin.utils.Lists; +import io.snyk.eclipse.plugin.utils.SnykLogger; public class SnykLanguageServer extends ProcessStreamConnectionProvider implements StreamConnectionProvider { public static final String LANGUAGE_SERVER_ID = "io.snyk.languageserver"; diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java index 7a555d3a..6ba3390b 100644 --- a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java @@ -1,11 +1,13 @@ package io.snyk.languageserver.protocolextension; import java.io.File; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.eclipse.core.resources.IProject; @@ -30,12 +32,16 @@ import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.snyk.eclipse.plugin.SnykStartup; import io.snyk.eclipse.plugin.properties.preferences.Preferences; import io.snyk.eclipse.plugin.utils.SnykLogger; import io.snyk.eclipse.plugin.views.SnykView; import io.snyk.eclipse.plugin.wizards.SnykWizard; import io.snyk.languageserver.protocolextension.messageObjects.HasAuthenticatedParam; +import io.snyk.languageserver.protocolextension.messageObjects.OAuthToken; import io.snyk.languageserver.protocolextension.messageObjects.SnykIsAvailableCliParams; import io.snyk.languageserver.protocolextension.messageObjects.SnykTrustedFoldersParams; @@ -43,6 +49,7 @@ public class SnykExtendedLanguageClient extends LanguageClientImpl { private final ProgressManager progressMgr = new ProgressManager(); private static SnykExtendedLanguageClient instance = null; + private final ObjectMapper om = new ObjectMapper(); @SuppressWarnings("unused") // used in lsp4e language server instantiation public SnykExtendedLanguageClient() { @@ -63,7 +70,7 @@ public void triggerScan(IWorkbenchWindow window) { executeCommand("snyk.workspace.scan", new ArrayList<>()); return; } - + ISelectionService service = window.getSelectionService(); IStructuredSelection structured = (IStructuredSelection) service.getSelection(); @@ -72,14 +79,14 @@ public void triggerScan(IWorkbenchWindow window) { if (firstElement instanceof JavaProject) { project = ((JavaProject) firstElement).getProject(); } - + if (firstElement instanceof IProject) { project = (IProject) firstElement; } if (project != null) { runForProject(project.getName()); - executeCommand("snyk.workspaceFolder.scan", List.of(project.getLocation().toOSString())); + executeCommand("snyk.workspaceFolder.scan", List.of(project.getLocation().toOSString())); } } catch (Exception e) { SnykLogger.logError(e); @@ -98,12 +105,16 @@ public void trustWorkspaceFolders() { @JsonNotification(value = "$/snyk.hasAuthenticated") public void hasAuthenticated(HasAuthenticatedParam param) { - Preferences.getInstance().store(Preferences.AUTH_TOKEN_KEY, param.getToken()); + var prefs = Preferences.getInstance(); + prefs.store(Preferences.AUTH_TOKEN_KEY, param.getToken()); triggerScan(null); + if (!param.getToken().isBlank()) { showAuthenticatedMessage(); enableSnykViewRunActions(); } + + setAuthenticationMethod(param, prefs); } @JsonNotification(value = "$/snyk.isAvailableCli") @@ -173,6 +184,16 @@ private void executeCommand(@NonNull String command, List arguments) { } } + protected void setAuthenticationMethod(HasAuthenticatedParam param, Preferences prefs) { + // check if its a json token and store the auth method based on that + try { + om.readValue(param.getToken(), OAuthToken.class); + prefs.store(Preferences.AUTHENTICATION_METHOD, Preferences.AUTH_METHOD_OAUTH); + } catch (JsonProcessingException e) { + prefs.store(Preferences.AUTHENTICATION_METHOD, Preferences.AUTH_METHOD_TOKEN); + } + } + // TODO: remove once LSP4e supports `showDocument` in its next release (it's // been merged to it already) @Override @@ -190,4 +211,27 @@ public CompletableFuture showDocument(ShowDocumentParams par }); } + /** + * Refresh the token using language server. Waits up to 2s for the token change. + * @return true if token has changed, false if not + */ + public boolean refreshOAuthToken() { + var p = Preferences.getInstance(); + var token = p.getAuthToken(); + executeCommand("snyk.oauthRefreshCommand", new ArrayList<>()); + // wait until token has changed or 2s have passed + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + while (token.equals(p.getAuthToken())) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + return p.getAuthToken(); + }); + var newToken = future.completeOnTimeout(token, 2, TimeUnit.SECONDS).join(); + return !token.equals(newToken); + } + } diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/OAuthToken.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/OAuthToken.java new file mode 100644 index 00000000..2620872d --- /dev/null +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/OAuthToken.java @@ -0,0 +1,76 @@ +package io.snyk.languageserver.protocolextension.messageObjects; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OAuthToken { + // AccessToken is the token that authorizes and authenticates + // the requests. + @JsonProperty("access_token") + private String accessToken; + + // TokenType is the type of token. + // The Type method returns either this or "Bearer", the default. + @JsonProperty("token_type") + private String tokenType; + + // RefreshToken is a token that's used by the application + // (as opposed to the user) to refresh the access token + // if it expires. + @JsonProperty("refresh_token") + private String refreshToken; + + // Expiry is the optional expiration time of the access token. + // + // If null, TokenSource implementations will reuse the same + // token forever and RefreshToken or equivalent + // mechanisms for that TokenSource will not be used. + private String expiry; + + // raw optionally contains extra metadata from the server + // when updating a token. + private Object raw; + + public void Token() { + // default constructor + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getExpiry() { + return expiry; + } + + public void setExpiry(String expiry) { + this.expiry = expiry; + } + + public Object getRaw() { + return raw; + } + + public void setRaw(Object raw) { + this.raw = raw; + } +} diff --git a/tests/src/test/java/io/snyk/eclipse/plugin/properties/preferences/PreferencesTest.java b/tests/src/test/java/io/snyk/eclipse/plugin/properties/preferences/PreferencesTest.java index 3a098444..b3ae704c 100644 --- a/tests/src/test/java/io/snyk/eclipse/plugin/properties/preferences/PreferencesTest.java +++ b/tests/src/test/java/io/snyk/eclipse/plugin/properties/preferences/PreferencesTest.java @@ -33,6 +33,7 @@ void test_DefaultPreferences() { assertEquals("true", prefs.getPref(Preferences.MANAGE_BINARIES_AUTOMATICALLY)); assertEquals("true", prefs.getPref(Preferences.MANAGE_BINARIES_AUTOMATICALLY)); assertEquals("1", prefs.getPref(Preferences.LSP_VERSION)); + assertEquals(Preferences.AUTH_METHOD_TOKEN, prefs.getPref(Preferences.AUTHENTICATION_METHOD)); assertTrue(prefs.getPref(Preferences.LS_BINARY_KEY).endsWith("/.snyk/snyk-ls") || prefs.getPref(Preferences.LS_BINARY_KEY).endsWith("snyk-ls.exe")); } @@ -68,7 +69,7 @@ void test_ExistingOrgInEnvironment_IsStoredInPreferences() { assertEquals(prefs.getPref(Preferences.ORGANIZATION_KEY), "myOrg"); } } - + @Test void test_GetBoolean_returnsBooleanProperty() { Preferences prefs = Preferences.getInstance(new InMemoryPreferenceStore()); @@ -76,11 +77,11 @@ void test_GetBoolean_returnsBooleanProperty() { assertFalse(prefs.getBooleanPref(Preferences.ACTIVATE_SNYK_CODE)); assertTrue(prefs.getBooleanPref(Preferences.ACTIVATE_SNYK_OPEN_SOURCE)); } - + @Test void test_GetBoolean_returnsFalseForNonBooleanProperty() { Preferences prefs = Preferences.getInstance(new InMemoryPreferenceStore()); - + assertFalse(prefs.getBooleanPref(Preferences.CLI_PATH)); } } diff --git a/tests/src/test/java/io/snyk/eclipse/plugin/runner/ProcessRunnerTest.java b/tests/src/test/java/io/snyk/eclipse/plugin/runner/ProcessRunnerTest.java index 9090b63d..c274fd57 100644 --- a/tests/src/test/java/io/snyk/eclipse/plugin/runner/ProcessRunnerTest.java +++ b/tests/src/test/java/io/snyk/eclipse/plugin/runner/ProcessRunnerTest.java @@ -1,5 +1,6 @@ package io.snyk.eclipse.plugin.runner; +import io.snyk.eclipse.plugin.EnvironmentConstants; import io.snyk.eclipse.plugin.properties.preferences.Preferences; import io.snyk.eclipse.plugin.properties.preferences.PreferencesUtils; import io.snyk.languageserver.LsRuntimeEnvironment; @@ -30,7 +31,7 @@ class ProcessRunnerTest { private IProxyService proxyServiceMock; @BeforeEach - void setUp() { + void setUp() { preferenceMock = mock(Preferences.class); PreferencesUtils.setPreferences(preferenceMock); @@ -38,10 +39,12 @@ void setUp() { when(preferenceMock.getPref(Preferences.ENABLE_TELEMETRY)).thenReturn("true"); when(preferenceMock.getPref(Preferences.ORGANIZATION_KEY)).thenReturn("organization"); when(preferenceMock.getPref(Preferences.INSECURE_KEY)).thenReturn("true"); + when(preferenceMock.getAuthToken()).thenReturn("token"); when(preferenceMock.getPref(Preferences.AUTH_TOKEN_KEY)).thenReturn("token"); when(preferenceMock.getPref(Preferences.ENDPOINT_KEY)).thenReturn("https://endpoint.io"); + when(preferenceMock.getPref(Preferences.AUTHENTICATION_METHOD)).thenReturn(Preferences.AUTH_METHOD_TOKEN); when(preferenceMock.getCliPath()).thenReturn(""); - + environmentMock = mock(LsRuntimeEnvironment.class); proxyServiceMock = mock(IProxyService.class); when(environmentMock.getProxyService()).thenReturn(proxyServiceMock); @@ -103,4 +106,24 @@ void testGetProcessBuilderLinuxNoOrg() { assertEquals(null, env.get(Preferences.ORGANIZATION_KEY)); } + @Test + void testOAuthEnabled() { + String expectedToken = "{\"access_token\":\"configAccessToken\",\"token_type\":\"Bearer\",\"refresh_token\":\"configRefreshToken\",\"expiry\":\"3023-03-29T17:47:13.714448+02:00\"}"; + + when(preferenceMock.getAuthToken()).thenReturn(expectedToken); + when(preferenceMock.getPref(Preferences.AUTHENTICATION_METHOD)).thenReturn(Preferences.AUTH_METHOD_OAUTH); + + ILog logger = mock(ILog.class); + Bundle bundle = mock(Bundle.class); + + when(bundle.getVersion()).thenReturn(new Version(2, 0, 0)); + + ProcessRunner cut = new ProcessRunner(bundle, logger, environmentMock); + ProcessBuilder builder = cut.createLinuxProcessBuilder(List.of("test"), Optional.of("good:path")); + + var env = builder.environment(); + + assertEquals("1", env.get(EnvironmentConstants.ENV_INTERNAL_SNYK_OAUTH_ENABLED)); + assertEquals(expectedToken, env.get(EnvironmentConstants.ENV_INTERNAL_OAUTH_TOKEN_STORAGE)); + } } diff --git a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java index 2c89899d..b2e87080 100644 --- a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java +++ b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java @@ -16,31 +16,66 @@ class SnykExtendedLanguageClientTest { private InMemoryPreferenceStore store = new InMemoryPreferenceStore(); private SnykExtendedLanguageClient cut = new SnykExtendedLanguageClient(); - + private Preferences pref; + @BeforeEach void setUp() { store = new InMemoryPreferenceStore(); - PreferencesUtils.setPreferences(Preferences.getInstance(store)); + pref = Preferences.getInstance(store); + PreferencesUtils.setPreferences(pref); } @Test void testAddTrustedPathsAddsPathToPreferenceStore() { SnykTrustedFoldersParams param = new SnykTrustedFoldersParams(); - param.setTrustedFolders(new String[] {"trusted/path "}); - + param.setTrustedFolders(new String[] { "trusted/path " }); + cut.addTrustedPaths(param); - + assertEquals("trusted/path", store.getString(Preferences.TRUSTED_FOLDERS, "")); } - + @Test void testAddTrustedPathsDeduplicatesAndTrims() { SnykTrustedFoldersParams param = new SnykTrustedFoldersParams(); - param.setTrustedFolders(new String[] {"trusted/path", "trusted/path", " trusted/path "}); - + param.setTrustedFolders(new String[] { "trusted/path", "trusted/path", " trusted/path " }); + cut.addTrustedPaths(param); - + assertEquals("trusted/path", store.getString(Preferences.TRUSTED_FOLDERS, "")); } + @Test + void testSetsApiToken() { + HasAuthenticatedParam param = new HasAuthenticatedParam(); + + param.setToken("apiToken"); + + cut.setAuthenticationMethod(param, pref); + + assertEquals(Preferences.AUTH_METHOD_TOKEN, store.getString(Preferences.AUTHENTICATION_METHOD, "")); + } + + @Test + void testSetsBlankToken() { + HasAuthenticatedParam param = new HasAuthenticatedParam(); + + param.setToken(""); + + cut.setAuthenticationMethod(param, pref); + + assertEquals(Preferences.AUTH_METHOD_TOKEN, store.getString(Preferences.AUTHENTICATION_METHOD, "")); + } + + @Test + void testSetsOAuthToken() { + HasAuthenticatedParam param = new HasAuthenticatedParam(); + String oAuthToken = + "{\"access_token\":\"configAccessToken\",\"token_type\":\"Bearer\",\"refresh_token\":\"configRefreshToken\",\"expiry\":\"3023-03-29T17:47:13.714448+02:00\"}"; + param.setToken(oAuthToken); + + cut.setAuthenticationMethod(param, pref); + + assertEquals(Preferences.AUTH_METHOD_OAUTH, store.getString(Preferences.AUTHENTICATION_METHOD, "")); + } }