-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Asana oAuth backend. #7049
Merged
Merged
Asana oAuth backend. #7049
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
airbyte-oauth/src/main/java/io/airbyte/oauth/flows/AsanaOAuthFlow.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.google.common.annotations.VisibleForTesting; | ||
import com.google.common.collect.ImmutableMap; | ||
import io.airbyte.commons.json.Jsons; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.oauth.BaseOAuthFlow; | ||
import java.io.IOException; | ||
import java.net.URISyntaxException; | ||
import java.net.http.HttpClient; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import java.util.function.Supplier; | ||
import org.apache.http.client.utils.URIBuilder; | ||
|
||
/** | ||
* Following docs from https://developers.asana.com/docs/oauth | ||
*/ | ||
public class AsanaOAuthFlow extends BaseOAuthFlow { | ||
|
||
private static final String AUTHORIZE_URL = "https://app.asana.com/-/oauth_authorize"; | ||
private static final String ACCESS_TOKEN_URL = "https://app.asana.com/-/oauth_token"; | ||
|
||
public AsanaOAuthFlow(ConfigRepository configRepository) { | ||
super(configRepository); | ||
} | ||
|
||
@VisibleForTesting | ||
AsanaOAuthFlow(ConfigRepository configRepository, HttpClient httpClient, Supplier<String> stateSupplier) { | ||
super(configRepository, httpClient, stateSupplier); | ||
} | ||
|
||
@Override | ||
protected String formatConsentUrl(UUID definitionId, String clientId, String redirectUrl) throws IOException { | ||
try { | ||
return new URIBuilder(AUTHORIZE_URL) | ||
.addParameter("client_id", clientId) | ||
.addParameter("redirect_uri", redirectUrl) | ||
.addParameter("response_type", "code") | ||
.addParameter("state", getState()) | ||
.build().toString(); | ||
} catch (URISyntaxException e) { | ||
throw new IOException("Failed to format Consent URL for OAuth flow", e); | ||
} | ||
} | ||
|
||
@Override | ||
protected String getAccessTokenUrl() { | ||
return ACCESS_TOKEN_URL; | ||
} | ||
|
||
protected Map<String, String> getAccessTokenQueryParameters(String clientId, String clientSecret, String authCode, String redirectUrl) { | ||
avida marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return ImmutableMap.<String, String>builder() | ||
.putAll(super.getAccessTokenQueryParameters(clientId, clientSecret, authCode, redirectUrl)) | ||
.put("grant_type", "authorization_code") | ||
.build(); | ||
} | ||
|
||
@Override | ||
protected Map<String, Object> extractRefreshToken(JsonNode data) throws IOException { | ||
System.out.println(Jsons.serialize(data)); | ||
if (data.has("refresh_token")) { | ||
final String refreshToken = data.get("refresh_token").asText(); | ||
return Map.of("credentials", Map.of("refresh_token", refreshToken)); | ||
} else { | ||
throw new IOException(String.format("Missing 'refresh_token' in query params from %s", ACCESS_TOKEN_URL)); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
173 changes: 173 additions & 0 deletions
173
...oauth/src/test-integration/java/io.airbyte.oauth.flows/AsanaOAuthFlowIntegrationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
/* | ||
* Copyright (c) 2021 Airbyte, Inc., all rights reserved. | ||
*/ | ||
|
||
package io.airbyte.oauth.flows; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
|
||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.google.common.collect.ImmutableMap; | ||
import com.sun.net.httpserver.HttpExchange; | ||
import com.sun.net.httpserver.HttpHandler; | ||
import com.sun.net.httpserver.HttpServer; | ||
import io.airbyte.commons.json.Jsons; | ||
import io.airbyte.config.SourceOAuthParameter; | ||
import io.airbyte.config.persistence.ConfigNotFoundException; | ||
import io.airbyte.config.persistence.ConfigRepository; | ||
import io.airbyte.validation.json.JsonValidationException; | ||
import java.io.IOException; | ||
import java.io.OutputStream; | ||
import java.net.InetSocketAddress; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.UUID; | ||
import org.junit.jupiter.api.AfterEach; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public class AsanaOAuthFlowIntegrationTest { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(AsanaOAuthFlowIntegrationTest.class); | ||
private static final String REDIRECT_URL = "http://localhost:8000/code"; | ||
private static final Path CREDENTIALS_PATH = Path.of("secrets/asana.json"); | ||
|
||
private ConfigRepository configRepository; | ||
private AsanaOAuthFlow asanaOAuthFlow; | ||
private HttpServer server; | ||
private ServerHandler serverHandler; | ||
|
||
@BeforeEach | ||
public void setup() throws IOException { | ||
if (!Files.exists(CREDENTIALS_PATH)) { | ||
throw new IllegalStateException( | ||
"Must provide path to a oauth credentials file."); | ||
} | ||
configRepository = mock(ConfigRepository.class); | ||
asanaOAuthFlow = new AsanaOAuthFlow(configRepository); | ||
|
||
server = HttpServer.create(new InetSocketAddress(8000), 0); | ||
server.setExecutor(null); // creates a default executor | ||
server.start(); | ||
serverHandler = new ServerHandler("code"); | ||
server.createContext("/code", serverHandler); | ||
} | ||
|
||
@AfterEach | ||
void tearDown() { | ||
server.stop(1); | ||
} | ||
|
||
@Test | ||
public void testFullAsanaOAuthFlow() throws InterruptedException, ConfigNotFoundException, IOException, JsonValidationException { | ||
int limit = 20; | ||
final UUID workspaceId = UUID.randomUUID(); | ||
final UUID definitionId = UUID.randomUUID(); | ||
final String fullConfigAsString = new String(Files.readAllBytes(CREDENTIALS_PATH)); | ||
final JsonNode credentialsJson = Jsons.deserialize(fullConfigAsString); | ||
final String clientId = credentialsJson.get("client_id").asText(); | ||
when(configRepository.listSourceOAuthParam()).thenReturn(List.of(new SourceOAuthParameter() | ||
.withOauthParameterId(UUID.randomUUID()) | ||
.withSourceDefinitionId(definitionId) | ||
.withWorkspaceId(workspaceId) | ||
.withConfiguration(Jsons.jsonNode(ImmutableMap.builder() | ||
.put("client_id", clientId) | ||
.put("client_secret", credentialsJson.get("client_secret").asText()) | ||
.build())))); | ||
final String url = asanaOAuthFlow.getSourceConsentUrl(workspaceId, definitionId, REDIRECT_URL); | ||
LOGGER.info("Waiting for user consent at: {}", url); | ||
// TODO: To automate, start a selenium job to navigate to the Consent URL and click on allowing | ||
// access... | ||
while (!serverHandler.isSucceeded() && limit > 0) { | ||
Thread.sleep(1000); | ||
limit -= 1; | ||
} | ||
assertTrue(serverHandler.isSucceeded(), "Failed to get User consent on time"); | ||
final Map<String, Object> params = asanaOAuthFlow.completeSourceOAuth(workspaceId, definitionId, | ||
Map.of("code", serverHandler.getParamValue()), REDIRECT_URL); | ||
LOGGER.info("Response from completing OAuth Flow is: {}", params.toString()); | ||
assertTrue(params.containsKey("credentials")); | ||
final Map creds = (Map) params.get("credentials"); | ||
assertTrue(creds.containsKey("refresh_token")); | ||
assertTrue(creds.get("refresh_token").toString().length() > 0); | ||
} | ||
|
||
static class ServerHandler implements HttpHandler { | ||
|
||
final private String expectedParam; | ||
private Map responseQuery; | ||
private String paramValue; | ||
private boolean succeeded; | ||
|
||
public ServerHandler(String expectedParam) { | ||
this.expectedParam = expectedParam; | ||
this.paramValue = ""; | ||
this.succeeded = false; | ||
} | ||
|
||
public boolean isSucceeded() { | ||
return succeeded; | ||
} | ||
|
||
public String getParamValue() { | ||
return paramValue; | ||
} | ||
|
||
public Map getResponseQuery() { | ||
return responseQuery; | ||
} | ||
|
||
@Override | ||
public void handle(HttpExchange t) { | ||
final String query = t.getRequestURI().getQuery(); | ||
LOGGER.info("Received query: '{}'", query); | ||
final Map<String, String> data; | ||
try { | ||
data = deserialize(query); | ||
final String response; | ||
if (data != null && data.containsKey(expectedParam)) { | ||
paramValue = data.get(expectedParam); | ||
response = String.format("Successfully extracted %s:\n'%s'\nTest should be continuing the OAuth Flow to retrieve the refresh_token...", | ||
expectedParam, paramValue); | ||
responseQuery = data; | ||
LOGGER.info(response); | ||
t.sendResponseHeaders(200, response.length()); | ||
succeeded = true; | ||
} else { | ||
response = String.format("Unable to parse query params from redirected url: %s", query); | ||
t.sendResponseHeaders(500, response.length()); | ||
} | ||
final OutputStream os = t.getResponseBody(); | ||
os.write(response.getBytes()); | ||
os.close(); | ||
} catch (RuntimeException | IOException e) { | ||
LOGGER.error("Failed to parse from body {}", query, e); | ||
} | ||
} | ||
|
||
private static Map<String, String> deserialize(String query) { | ||
if (query == null) { | ||
return null; | ||
} | ||
final Map<String, String> result = new HashMap<>(); | ||
for (String param : query.split("&")) { | ||
String[] entry = param.split("="); | ||
if (entry.length > 1) { | ||
result.put(entry[0], entry[1]); | ||
} else { | ||
result.put(entry[0], ""); | ||
} | ||
} | ||
return result; | ||
} | ||
|
||
} | ||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we list them alphabetically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done