From 30c0659a661f9f20211cc85a477488427e2888db Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Mon, 18 Oct 2021 18:03:31 +0100 Subject: [PATCH 1/8] adding google sheets oauth flow to server --- .../oauth/OAuthImplementationFactory.java | 2 + .../flows/google/GoogleSheetsOAuthFlow.java | 55 ++++++ .../GoogleSheetsOAuthFlowIntegrationTest.java | 168 ++++++++++++++++++ .../google/GoogleSheetsOAuthFlowTest.java | 117 ++++++++++++ 4 files changed, 342 insertions(+) create mode 100644 airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java create mode 100644 airbyte-oauth/src/test-integration/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowIntegrationTest.java create mode 100644 airbyte-oauth/src/test/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowTest.java diff --git a/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java b/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java index 785ad5f0d229f1..b41068878af689 100644 --- a/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java @@ -11,6 +11,7 @@ import io.airbyte.oauth.flows.google.GoogleAdsOAuthFlow; import io.airbyte.oauth.flows.google.GoogleAnalyticsOAuthFlow; import io.airbyte.oauth.flows.google.GoogleSearchConsoleOAuthFlow; +import io.airbyte.oauth.flows.google.GoogleSheetsOAuthFlow; import java.util.Map; public class OAuthImplementationFactory { @@ -23,6 +24,7 @@ public OAuthImplementationFactory(final ConfigRepository configRepository) { .put("airbyte/source-google-ads", new GoogleAdsOAuthFlow(configRepository)) .put("airbyte/source-google-analytics-v4", new GoogleAnalyticsOAuthFlow(configRepository)) .put("airbyte/source-google-search-console", new GoogleSearchConsoleOAuthFlow(configRepository)) + .put("airbyte/source-google-sheets", new GoogleSheetsOAuthFlow(configRepository)) .put("airbyte/source-trello", new TrelloOAuthFlow(configRepository)) .build(); } diff --git a/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java new file mode 100644 index 00000000000000..e19f1757092802 --- /dev/null +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.oauth.flows.google; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import io.airbyte.config.persistence.ConfigRepository; +import java.io.IOException; +import java.net.http.HttpClient; +import java.util.Map; +import java.util.function.Supplier; + +public class GoogleSheetsOAuthFlow extends GoogleOAuthFlow { + + @VisibleForTesting + static final String SCOPE_URL = "https://www.googleapis.com/auth/spreadsheets.readonly"; + + public GoogleSheetsOAuthFlow(final ConfigRepository configRepository) { + super(configRepository); + } + + @VisibleForTesting + GoogleSheetsOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient, final Supplier stateSupplier) { + super(configRepository, httpClient, stateSupplier); + } + + @Override + protected String getScope() { + return SCOPE_URL; + } + + @Override + protected String getClientIdUnsafe(final JsonNode config) { + // the config object containing client ID and secret is nested inside the "credentials" object + Preconditions.checkArgument(config.hasNonNull("credentials")); + return super.getClientIdUnsafe(config.get("credentials")); + } + + @Override + protected String getClientSecretUnsafe(final JsonNode config) { + // the config object containing client ID and secret is nested inside the "credentials" object + Preconditions.checkArgument(config.hasNonNull("credentials")); + return super.getClientSecretUnsafe(config.get("credentials")); + } + + @Override + protected Map extractRefreshToken(final JsonNode data) throws IOException { + // the config object containing refresh token is nested inside the "credentials" object + return Map.of("credentials", super.extractRefreshToken(data)); + } + +} diff --git a/airbyte-oauth/src/test-integration/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowIntegrationTest.java b/airbyte-oauth/src/test-integration/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowIntegrationTest.java new file mode 100644 index 00000000000000..3d4a84b44ee302 --- /dev/null +++ b/airbyte-oauth/src/test-integration/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowIntegrationTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2021 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.oauth.flows.google; + +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 GoogleSheetsOAuthFlowIntegrationTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(GoogleSheetsOAuthFlowIntegrationTest.class); + private static final String REDIRECT_URL = "http://localhost/code"; + private static final Path CREDENTIALS_PATH = Path.of("secrets/google_sheets.json"); + + private ConfigRepository configRepository; + private GoogleSheetsOAuthFlow googleSheetsOAuthFlow; + 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); + googleSheetsOAuthFlow = new GoogleSheetsOAuthFlow(configRepository); + + server = HttpServer.create(new InetSocketAddress(80), 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 testFullGoogleOAuthFlow() 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); + when(configRepository.listSourceOAuthParam()).thenReturn(List.of(new SourceOAuthParameter() + .withOauthParameterId(UUID.randomUUID()) + .withSourceDefinitionId(definitionId) + .withWorkspaceId(workspaceId) + .withConfiguration(Jsons.jsonNode(Map.of("credentials", ImmutableMap.builder() + .put("client_id", credentialsJson.get("credentials").get("client_id").asText()) + .put("client_secret", credentialsJson.get("credentials").get("client_secret").asText()) + .build()))))); + final String url = googleSheetsOAuthFlow.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 params = googleSheetsOAuthFlow.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 credentials = (Map) params.get("credentials"); + assertTrue(credentials.containsKey("refresh_token")); + assertTrue(credentials.get("refresh_token").toString().length() > 0); + assertTrue(credentials.containsKey("access_token")); + assertTrue(credentials.get("access_token").toString().length() > 0); + } + + static class ServerHandler implements HttpHandler { + + final private String expectedParam; + private String paramValue; + private boolean succeeded; + + public ServerHandler(final String expectedParam) { + this.expectedParam = expectedParam; + this.paramValue = ""; + this.succeeded = false; + } + + public boolean isSucceeded() { + return succeeded; + } + + public String getParamValue() { + return paramValue; + } + + @Override + public void handle(final HttpExchange t) { + final String query = t.getRequestURI().getQuery(); + LOGGER.info("Received query: '{}'", query); + final Map 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); + 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 (final RuntimeException | IOException e) { + LOGGER.error("Failed to parse from body {}", query, e); + } + } + + private static Map deserialize(final String query) { + if (query == null) { + return null; + } + final Map result = new HashMap<>(); + for (final String param : query.split("&")) { + final String[] entry = param.split("="); + if (entry.length > 1) { + result.put(entry[0], entry[1]); + } else { + result.put(entry[0], ""); + } + } + return result; + } + + } + +} diff --git a/airbyte-oauth/src/test/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowTest.java b/airbyte-oauth/src/test/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowTest.java new file mode 100644 index 00000000000000..3a6e1ee48961dc --- /dev/null +++ b/airbyte-oauth/src/test/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlowTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2021 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.oauth.flows.google; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.airbyte.commons.json.Jsons; +import io.airbyte.config.DestinationOAuthParameter; +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.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class GoogleSheetsOAuthFlowTest { + + private static final String REDIRECT_URL = "https://airbyte.io"; + + private HttpClient httpClient; + private ConfigRepository configRepository; + private GoogleSheetsOAuthFlow googleSheetsOAuthFlow; + + private UUID workspaceId; + private UUID definitionId; + + @BeforeEach + public void setup() { + httpClient = mock(HttpClient.class); + configRepository = mock(ConfigRepository.class); + googleSheetsOAuthFlow = new GoogleSheetsOAuthFlow(configRepository, httpClient, GoogleSheetsOAuthFlowTest::getConstantState); + + workspaceId = UUID.randomUUID(); + definitionId = UUID.randomUUID(); + } + + private static String getConstantState() { + return "state"; + } + + @Test + public void testCompleteSourceOAuth() throws IOException, ConfigNotFoundException, JsonValidationException, InterruptedException { + when(configRepository.listSourceOAuthParam()).thenReturn(List.of(new SourceOAuthParameter() + .withOauthParameterId(UUID.randomUUID()) + .withSourceDefinitionId(definitionId) + .withWorkspaceId(workspaceId) + .withConfiguration(Jsons.jsonNode(Map.of("credentials", ImmutableMap.builder() + .put("client_id", "test_client_id") + .put("client_secret", "test_client_secret") + .build()))))); + + final Map returnedCredentials = Map.of("refresh_token", "refresh_token_response"); + final HttpResponse response = mock(HttpResponse.class); + when(response.body()).thenReturn(Jsons.serialize(returnedCredentials)); + when(httpClient.send(any(), any())).thenReturn(response); + final Map queryParams = Map.of("code", "test_code"); + final Map actualQueryParams = googleSheetsOAuthFlow.completeSourceOAuth(workspaceId, definitionId, queryParams, REDIRECT_URL); + + assertEquals(Jsons.serialize(Map.of("credentials", returnedCredentials)), Jsons.serialize(actualQueryParams)); + } + + @Test + public void testCompleteDestinationOAuth() throws IOException, ConfigNotFoundException, JsonValidationException, InterruptedException { + when(configRepository.listDestinationOAuthParam()).thenReturn(List.of(new DestinationOAuthParameter() + .withOauthParameterId(UUID.randomUUID()) + .withDestinationDefinitionId(definitionId) + .withWorkspaceId(workspaceId) + .withConfiguration(Jsons.jsonNode(Map.of("credentials", ImmutableMap.builder() + .put("client_id", "test_client_id") + .put("client_secret", "test_client_secret") + .build()))))); + + final Map returnedCredentials = Map.of("refresh_token", "refresh_token_response"); + final HttpResponse response = mock(HttpResponse.class); + when(response.body()).thenReturn(Jsons.serialize(returnedCredentials)); + when(httpClient.send(any(), any())).thenReturn(response); + final Map queryParams = Map.of("code", "test_code"); + final Map actualQueryParams = + googleSheetsOAuthFlow.completeDestinationOAuth(workspaceId, definitionId, queryParams, REDIRECT_URL); + + assertEquals(Jsons.serialize(Map.of("credentials", returnedCredentials)), Jsons.serialize(actualQueryParams)); + } + + @Test + public void testGetClientIdUnsafe() { + final String clientId = "123"; + final Map clientIdMap = Map.of("client_id", clientId); + final Map> nestedConfig = Map.of("credentials", clientIdMap); + + assertThrows(IllegalArgumentException.class, () -> googleSheetsOAuthFlow.getClientIdUnsafe(Jsons.jsonNode(clientIdMap))); + assertEquals(clientId, googleSheetsOAuthFlow.getClientIdUnsafe(Jsons.jsonNode(nestedConfig))); + } + + @Test + public void testGetClientSecretUnsafe() { + final String clientSecret = "secret"; + final Map clientIdMap = Map.of("client_secret", clientSecret); + final Map> nestedConfig = Map.of("credentials", clientIdMap); + + assertThrows(IllegalArgumentException.class, () -> googleSheetsOAuthFlow.getClientSecretUnsafe(Jsons.jsonNode(clientIdMap))); + assertEquals(clientSecret, googleSheetsOAuthFlow.getClientSecretUnsafe(Jsons.jsonNode(nestedConfig))); + } + +} From 44f6f44f58803d0a5ec7ffadf06da8a9d80b822d Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Mon, 18 Oct 2021 23:35:33 +0100 Subject: [PATCH 2/8] fix oauth type in protocol yaml --- .../src/main/resources/airbyte_protocol/airbyte_protocol.yaml | 2 +- .../scheduler/client/BucketSpecCacheSchedulerClient.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml b/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml index ab7ff643c29383..5df0a3d0ee2329 100644 --- a/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml +++ b/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml @@ -227,7 +227,7 @@ definitions: " type: array items: - type: string + type: [string, integer] oauthFlowInitParameters: description: "Pointers to the fields in the rootObject needed to obtain the initial refresh/access tokens for the OAuth flow. diff --git a/airbyte-scheduler/client/src/main/java/io/airbyte/scheduler/client/BucketSpecCacheSchedulerClient.java b/airbyte-scheduler/client/src/main/java/io/airbyte/scheduler/client/BucketSpecCacheSchedulerClient.java index 3f50443325a9c6..6d677cd82254df 100644 --- a/airbyte-scheduler/client/src/main/java/io/airbyte/scheduler/client/BucketSpecCacheSchedulerClient.java +++ b/airbyte-scheduler/client/src/main/java/io/airbyte/scheduler/client/BucketSpecCacheSchedulerClient.java @@ -127,7 +127,7 @@ private static Optional attemptToFetchSpecFromBucket(fin try { validateConfig(Jsons.deserialize(specAsString)); } catch (final JsonValidationException e) { - LOGGER.error("Received invalid spec from bucket store. Received: {}", specAsString); + LOGGER.error("Received invalid spec from bucket store. {}", e.toString()); return Optional.empty(); } return Optional.of(Jsons.deserialize(specAsString, ConnectorSpecification.class)); From 3a5034aa7e66248b2aed252b9f12a862643f2489 Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Mon, 18 Oct 2021 23:36:03 +0100 Subject: [PATCH 3/8] bump sheets version in definitions --- .../init/src/main/resources/seed/source_definitions.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml index c51352038bce53..3909e28327f525 100644 --- a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml @@ -105,7 +105,7 @@ - sourceDefinitionId: 71607ba1-c0ac-4799-8049-7f4b90dd50f7 name: Google Sheets dockerRepository: airbyte/source-google-sheets - dockerImageTag: 0.2.5 + dockerImageTag: 0.2.6 documentationUrl: https://docs.airbyte.io/integrations/sources/google-sheets icon: google-sheets.svg sourceType: file From b3230b472149c47ab1dc3da055849c7920513d60 Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Tue, 19 Oct 2021 11:54:55 +0100 Subject: [PATCH 4/8] added GDrive scope --- .../io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java index e19f1757092802..bf995ab93423be 100644 --- a/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java @@ -15,8 +15,10 @@ public class GoogleSheetsOAuthFlow extends GoogleOAuthFlow { + // space-delimited string for multiple scopes, see: + // https://datatracker.ietf.org/doc/html/rfc6749#section-3.3 @VisibleForTesting - static final String SCOPE_URL = "https://www.googleapis.com/auth/spreadsheets.readonly"; + static final String SCOPE_URL = "https://www.googleapis.com/auth/spreadsheets.readonly https://www.googleapis.com/auth/drive.readonly"; public GoogleSheetsOAuthFlow(final ConfigRepository configRepository) { super(configRepository); From ab09b8bdfae58288b819b1781858bc798ee97ace Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Thu, 21 Oct 2021 14:39:04 +0100 Subject: [PATCH 5/8] update sheets to master changes --- .../airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java index bf995ab93423be..11e2dd08e88d92 100644 --- a/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/google/GoogleSheetsOAuthFlow.java @@ -8,9 +8,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import io.airbyte.config.persistence.ConfigRepository; -import java.io.IOException; import java.net.http.HttpClient; -import java.util.Map; import java.util.function.Supplier; public class GoogleSheetsOAuthFlow extends GoogleOAuthFlow { @@ -48,10 +46,4 @@ protected String getClientSecretUnsafe(final JsonNode config) { return super.getClientSecretUnsafe(config.get("credentials")); } - @Override - protected Map extractRefreshToken(final JsonNode data) throws IOException { - // the config object containing refresh token is nested inside the "credentials" object - return Map.of("credentials", super.extractRefreshToken(data)); - } - } From 1fd4a2d2022bb413c4525c940aa5f47a27cc2fc4 Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Thu, 21 Oct 2021 15:01:53 +0100 Subject: [PATCH 6/8] update protocol incl. cdk --- airbyte-cdk/python/CHANGELOG.md | 3 +++ airbyte-cdk/python/airbyte_cdk/models/airbyte_protocol.py | 4 ++-- airbyte-cdk/python/setup.py | 2 +- .../airbyte_protocol/models/airbyte_protocol.py | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/airbyte-cdk/python/CHANGELOG.md b/airbyte-cdk/python/CHANGELOG.md index f7d519cf29f2c3..27dbd908c3be66 100644 --- a/airbyte-cdk/python/CHANGELOG.md +++ b/airbyte-cdk/python/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.1.30 +Updated OAuth2Specification.rootObject type in airbyte_protocol to allow string or int + ## 0.1.29 Fix import logger error diff --git a/airbyte-cdk/python/airbyte_cdk/models/airbyte_protocol.py b/airbyte-cdk/python/airbyte_cdk/models/airbyte_protocol.py index 18fa6d866f15e5..de14f3043ae918 100644 --- a/airbyte-cdk/python/airbyte_cdk/models/airbyte_protocol.py +++ b/airbyte-cdk/python/airbyte_cdk/models/airbyte_protocol.py @@ -8,7 +8,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from pydantic import AnyUrl, BaseModel, Extra, Field @@ -89,7 +89,7 @@ class OAuth2Specification(BaseModel): class Config: extra = Extra.allow - rootObject: Optional[List[str]] = Field( + rootObject: Optional[List[Union[str, int]]] = Field( None, description="A list of strings representing a pointer to the root object which contains any oauth parameters in the ConnectorSpecification.\nExamples:\nif oauth parameters were contained inside the top level, rootObject=[] If they were nested inside another object {'credentials': {'app_id' etc...}, rootObject=['credentials'] If they were inside a oneOf {'switch': {oneOf: [{client_id...}, {non_oauth_param]}}, rootObject=['switch', 0] ", ) diff --git a/airbyte-cdk/python/setup.py b/airbyte-cdk/python/setup.py index 2c24676d6f46d4..4570265c97bf67 100644 --- a/airbyte-cdk/python/setup.py +++ b/airbyte-cdk/python/setup.py @@ -15,7 +15,7 @@ setup( name="airbyte-cdk", - version="0.1.29", + version="0.1.30", description="A framework for writing Airbyte Connectors.", long_description=README, long_description_content_type="text/markdown", diff --git a/airbyte-integrations/bases/airbyte-protocol/airbyte_protocol/models/airbyte_protocol.py b/airbyte-integrations/bases/airbyte-protocol/airbyte_protocol/models/airbyte_protocol.py index 18fa6d866f15e5..de14f3043ae918 100644 --- a/airbyte-integrations/bases/airbyte-protocol/airbyte_protocol/models/airbyte_protocol.py +++ b/airbyte-integrations/bases/airbyte-protocol/airbyte_protocol/models/airbyte_protocol.py @@ -8,7 +8,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from pydantic import AnyUrl, BaseModel, Extra, Field @@ -89,7 +89,7 @@ class OAuth2Specification(BaseModel): class Config: extra = Extra.allow - rootObject: Optional[List[str]] = Field( + rootObject: Optional[List[Union[str, int]]] = Field( None, description="A list of strings representing a pointer to the root object which contains any oauth parameters in the ConnectorSpecification.\nExamples:\nif oauth parameters were contained inside the top level, rootObject=[] If they were nested inside another object {'credentials': {'app_id' etc...}, rootObject=['credentials'] If they were inside a oneOf {'switch': {oneOf: [{client_id...}, {non_oauth_param]}}, rootObject=['switch', 0] ", ) From 99a6ce3d5b12c7c222b293e8fbc98dd2e2a25419 Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Thu, 21 Oct 2021 16:09:34 +0100 Subject: [PATCH 7/8] protocol typing for oauth rootobject --- airbyte-api/src/main/openapi/config.yaml | 6 ++++-- .../main/resources/airbyte_protocol/airbyte_protocol.yaml | 5 ++++- .../airbyte/server/converters/OauthModelConverterTest.java | 6 +++--- docs/reference/api/generated-api-html/index.html | 6 +++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/airbyte-api/src/main/openapi/config.yaml b/airbyte-api/src/main/openapi/config.yaml index 4d25a5c3bffe69..f6bf17441bf2fe 100644 --- a/airbyte-api/src/main/openapi/config.yaml +++ b/airbyte-api/src/main/openapi/config.yaml @@ -2023,8 +2023,10 @@ components: If they were inside a oneOf {'switch': {oneOf: [{client_id...}, {non_oauth_param]}}, rootObject=['switch', 0] " type: array - items: - type: string + items: {} # <--- using generic any type. Build fails with oneOf (https://github.com/OpenAPITools/openapi-generator/issues/6161) + example: + - path + - 1 oauthFlowInitParameters: description: "Pointers to the fields in the rootObject needed to obtain the initial refresh/access tokens for the OAuth flow. diff --git a/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml b/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml index 5df0a3d0ee2329..8b99b171028d3f 100644 --- a/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml +++ b/airbyte-protocol/models/src/main/resources/airbyte_protocol/airbyte_protocol.yaml @@ -227,7 +227,10 @@ definitions: " type: array items: - type: [string, integer] + oneOf: + - type: string + - type: integer + oauthFlowInitParameters: description: "Pointers to the fields in the rootObject needed to obtain the initial refresh/access tokens for the OAuth flow. diff --git a/airbyte-server/src/test/java/io/airbyte/server/converters/OauthModelConverterTest.java b/airbyte-server/src/test/java/io/airbyte/server/converters/OauthModelConverterTest.java index 46c93551952438..a4b5066013a8e5 100644 --- a/airbyte-server/src/test/java/io/airbyte/server/converters/OauthModelConverterTest.java +++ b/airbyte-server/src/test/java/io/airbyte/server/converters/OauthModelConverterTest.java @@ -24,7 +24,7 @@ private static Stream testProvider() { Arguments.of( List.of(List.of("init1"), List.of("init2-1", "init2-2")), List.of(List.of("output1"), List.of("output2-1", "output2-2")), - List.of("path")), + List.of("path", "nestedPath", 1)), // init params only Arguments.of( List.of(List.of("init1"), List.of("init2-1", "init2-2")), @@ -39,12 +39,12 @@ private static Stream testProvider() { Arguments.of( List.of(List.of()), List.of(List.of()), - List.of("path"))); + List.of("path", "nestedPath", 1))); } @ParameterizedTest @MethodSource("testProvider") - public void testIt(final List> initParams, final List> outputParams, final List rootObject) { + public void testIt(final List> initParams, final List> outputParams, final List rootObject) { final ConnectorSpecification input = new ConnectorSpecification().withAuthSpecification( new AuthSpecification() .withAuthType(AuthSpecification.AuthType.OAUTH_2_0) diff --git a/docs/reference/api/generated-api-html/index.html b/docs/reference/api/generated-api-html/index.html index 6f9146149159e7..a43ac05add712a 100644 --- a/docs/reference/api/generated-api-html/index.html +++ b/docs/reference/api/generated-api-html/index.html @@ -2534,7 +2534,7 @@

Example data

"auth_type" : "oauth2.0", "oauth2Specification" : { "oauthFlowOutputParameters" : [ [ "oauthFlowOutputParameters", "oauthFlowOutputParameters" ], [ "oauthFlowOutputParameters", "oauthFlowOutputParameters" ] ], - "rootObject" : [ "rootObject", "rootObject" ], + "rootObject" : [ "path", 1 ], "oauthFlowInitParameters" : [ [ "oauthFlowInitParameters", "oauthFlowInitParameters" ], [ "oauthFlowInitParameters", "oauthFlowInitParameters" ] ] } }, @@ -4960,7 +4960,7 @@

Example data

"auth_type" : "oauth2.0", "oauth2Specification" : { "oauthFlowOutputParameters" : [ [ "oauthFlowOutputParameters", "oauthFlowOutputParameters" ], [ "oauthFlowOutputParameters", "oauthFlowOutputParameters" ] ], - "rootObject" : [ "rootObject", "rootObject" ], + "rootObject" : [ "path", 1 ], "oauthFlowInitParameters" : [ [ "oauthFlowInitParameters", "oauthFlowInitParameters" ], [ "oauthFlowInitParameters", "oauthFlowInitParameters" ] ] } }, @@ -7292,7 +7292,7 @@

NotificationType - OAuth2Specification - Up

An object containing any metadata needed to describe this connector's Oauth flow
-
rootObject
array[String] A list of strings representing a pointer to the root object which contains any oauth parameters in the ConnectorSpecification. +
rootObject
array[oas_any_type_not_mapped] A list of strings representing a pointer to the root object which contains any oauth parameters in the ConnectorSpecification. Examples: if oauth parameters were contained inside the top level, rootObject=[] If they were nested inside another object {'credentials': {'app_id' etc...}, rootObject=['credentials'] If they were inside a oneOf {'switch': {oneOf: [{client_id...}, {non_oauth_param]}}, rootObject=['switch', 0]
oauthFlowInitParameters
array[array[String]] Pointers to the fields in the rootObject needed to obtain the initial refresh/access tokens for the OAuth flow. Each inner array represents the path in the rootObject of the referenced field. For example. Assume the rootObject contains params 'app_secret', 'app_id' which are needed to get the initial refresh token. If they are not nested in the rootObject, then the array would look like this [['app_secret'], ['app_id']] If they are nested inside an object called 'auth_params' then this array would be [['auth_params', 'app_secret'], ['auth_params', 'app_id']]
From a3c8b5571fdcffdb6af8821cc84b0fb0de9270dd Mon Sep 17 00:00:00 2001 From: George Claireaux Date: Thu, 21 Oct 2021 16:52:49 +0100 Subject: [PATCH 8/8] format --- airbyte-api/src/main/openapi/config.yaml | 2 +- .../source-klaviyo/integration_tests/invalid_config.json | 4 ++-- .../source-smartsheets/source_smartsheets/source.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/airbyte-api/src/main/openapi/config.yaml b/airbyte-api/src/main/openapi/config.yaml index f6bf17441bf2fe..ef75e61ba95210 100644 --- a/airbyte-api/src/main/openapi/config.yaml +++ b/airbyte-api/src/main/openapi/config.yaml @@ -2023,7 +2023,7 @@ components: If they were inside a oneOf {'switch': {oneOf: [{client_id...}, {non_oauth_param]}}, rootObject=['switch', 0] " type: array - items: {} # <--- using generic any type. Build fails with oneOf (https://github.com/OpenAPITools/openapi-generator/issues/6161) + items: {} # <--- using generic any type. Build fails with oneOf (https://github.com/OpenAPITools/openapi-generator/issues/6161) example: - path - 1 diff --git a/airbyte-integrations/connectors/source-klaviyo/integration_tests/invalid_config.json b/airbyte-integrations/connectors/source-klaviyo/integration_tests/invalid_config.json index daf9023ebdbfd9..43734e9980df50 100644 --- a/airbyte-integrations/connectors/source-klaviyo/integration_tests/invalid_config.json +++ b/airbyte-integrations/connectors/source-klaviyo/integration_tests/invalid_config.json @@ -1,4 +1,4 @@ { - "api_key": "api-key", - "start_date": "2021-01-01T00:00:00Z" + "api_key": "api-key", + "start_date": "2021-01-01T00:00:00Z" } diff --git a/airbyte-integrations/connectors/source-smartsheets/source_smartsheets/source.py b/airbyte-integrations/connectors/source-smartsheets/source_smartsheets/source.py index 7fb968465255bb..56f80fd8e7fc87 100644 --- a/airbyte-integrations/connectors/source-smartsheets/source_smartsheets/source.py +++ b/airbyte-integrations/connectors/source-smartsheets/source_smartsheets/source.py @@ -19,6 +19,7 @@ Status, Type, ) + # helpers from airbyte_cdk.sources import Source