Skip to content

Commit da34bef

Browse files
Implement Google Analytics & Google Ads OAuth Flow (#5911)
Co-authored-by: Sherif Nada <[email protected]>
1 parent 51e2718 commit da34bef

File tree

16 files changed

+1032
-91
lines changed

16 files changed

+1032
-91
lines changed

airbyte-api/src/main/openapi/config.yaml

+64-46
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,29 @@ paths:
11841184
$ref: "#/components/responses/NotFoundResponse"
11851185
"422":
11861186
$ref: "#/components/responses/InvalidInputResponse"
1187+
/v1/source_oauths/oauth_params/create:
1188+
post:
1189+
tags:
1190+
- oauth
1191+
summary: >
1192+
Sets instancewide variables to be used for the oauth flow when creating this source. When set, these variables will be injected
1193+
into a connector's configuration before any interaction with the connector image itself. This enables running oauth flows with
1194+
consistent variables e.g: the company's Google Ads developer_token, client_id, and client_secret without the user having to know
1195+
about these variables.
1196+
operationId: setInstancewideSourceOauthParams
1197+
requestBody:
1198+
content:
1199+
application/json:
1200+
schema:
1201+
$ref: "#/components/schemas/SetInstancewideSourceOauthParamsRequestBody"
1202+
required: true
1203+
responses:
1204+
"200":
1205+
description: Successful
1206+
"400":
1207+
$ref: "#/components/responses/ExceptionResponse"
1208+
"404":
1209+
$ref: "#/components/responses/NotFoundResponse"
11871210
/v1/source_oauths/get_consent_url:
11881211
post:
11891212
tags:
@@ -1276,6 +1299,29 @@ paths:
12761299
$ref: "#/components/responses/NotFoundResponse"
12771300
"422":
12781301
$ref: "#/components/responses/InvalidInputResponse"
1302+
/v1/destination_oauths/oauth_params/create:
1303+
post:
1304+
tags:
1305+
- oauth
1306+
summary: >
1307+
Sets instancewide variables to be used for the oauth flow when creating this destination. When set, these variables will be injected
1308+
into a connector's configuration before any interaction with the connector image itself. This enables running oauth flows with
1309+
consistent variables e.g: the company's Google Ads developer_token, client_id, and client_secret without the user having to know
1310+
about these variables.
1311+
operationId: setInstancewideDestinationOauthParams
1312+
requestBody:
1313+
content:
1314+
application/json:
1315+
schema:
1316+
$ref: "#/components/schemas/SetInstancewideDestinationOauthParamsRequestBody"
1317+
required: true
1318+
responses:
1319+
"200":
1320+
description: Successful
1321+
"400":
1322+
$ref: "#/components/responses/ExceptionResponse"
1323+
"404":
1324+
$ref: "#/components/responses/NotFoundResponse"
12791325
/v1/web_backend/connections/list:
12801326
post:
12811327
tags:
@@ -1620,52 +1666,6 @@ paths:
16201666
$ref: "#/components/schemas/ImportRead"
16211667
"404":
16221668
$ref: "#/components/responses/NotFoundResponse"
1623-
/v1/source_oauths/oauth_params/create:
1624-
post:
1625-
tags:
1626-
- oauth
1627-
summary: >
1628-
Sets instancewide variables to be used for the oauth flow when creating this source. When set, these variables will be injected
1629-
into a connector's configuration before any interaction with the connector image itself. This enables running oauth flows with
1630-
consistent variables e.g: the company's Google Ads developer_token, client_id, and client_secret without the user having to know
1631-
about these variables.
1632-
operationId: setInstancewideSourceOauthParams
1633-
requestBody:
1634-
content:
1635-
application/json:
1636-
schema:
1637-
$ref: "#/components/schemas/SetInstancewideSourceOauthParamsRequestBody"
1638-
required: true
1639-
responses:
1640-
"200":
1641-
description: Successful
1642-
"400":
1643-
$ref: "#/components/responses/ExceptionResponse"
1644-
"404":
1645-
$ref: "#/components/responses/NotFoundResponse"
1646-
/v1/destination_oauths/oauth_params/create:
1647-
post:
1648-
tags:
1649-
- oauth
1650-
summary: >
1651-
Sets instancewide variables to be used for the oauth flow when creating this destination. When set, these variables will be injected
1652-
into a connector's configuration before any interaction with the connector image itself. This enables running oauth flows with
1653-
consistent variables e.g: the company's Google Ads developer_token, client_id, and client_secret without the user having to know
1654-
about these variables.
1655-
operationId: setInstancewideDestinationOauthParams
1656-
requestBody:
1657-
content:
1658-
application/json:
1659-
schema:
1660-
$ref: "#/components/schemas/SetInstancewideDestinationOauthParamsRequestBody"
1661-
required: true
1662-
responses:
1663-
"200":
1664-
description: Successful
1665-
"400":
1666-
$ref: "#/components/responses/ExceptionResponse"
1667-
"404":
1668-
$ref: "#/components/responses/NotFoundResponse"
16691669
components:
16701670
securitySchemes:
16711671
bearerAuth:
@@ -2991,20 +2991,30 @@ components:
29912991
type: object
29922992
required:
29932993
- sourceDefinitionId
2994+
- workspaceId
2995+
- redirectUrl
29942996
properties:
29952997
sourceDefinitionId:
29962998
$ref: "#/components/schemas/SourceDefinitionId"
29972999
workspaceId:
29983000
$ref: "#/components/schemas/WorkspaceId"
3001+
redirectUrl:
3002+
description: The url to redirect to after getting the user consent
3003+
type: string
29993004
DestinationOauthConsentRequest:
30003005
type: object
30013006
required:
30023007
- destinationDefinitionId
3008+
- workspaceId
3009+
- redirectUrl
30033010
properties:
30043011
destinationDefinitionId:
30053012
$ref: "#/components/schemas/DestinationDefinitionId"
30063013
workspaceId:
30073014
$ref: "#/components/schemas/WorkspaceId"
3015+
redirectUrl:
3016+
description: The url to redirect to after getting the user consent
3017+
type: string
30083018
OAuthConsentRead:
30093019
type: object
30103020
required:
@@ -3016,11 +3026,15 @@ components:
30163026
type: object
30173027
required:
30183028
- sourceDefinitionId
3029+
- workspaceId
30193030
properties:
30203031
sourceDefinitionId:
30213032
$ref: "#/components/schemas/SourceDefinitionId"
30223033
workspaceId:
30233034
$ref: "#/components/schemas/WorkspaceId"
3035+
redirectUrl:
3036+
description: When completing OAuth flow to gain an access token, some API sometimes requires to verify that the app re-send the redirectUrl that was used when consent was given.
3037+
type: string
30243038
queryParams:
30253039
description: The query parameters present in the redirect URL after a user granted consent e.g auth code
30263040
type: object
@@ -3029,11 +3043,15 @@ components:
30293043
type: object
30303044
required:
30313045
- destinationDefinitionId
3046+
- workspaceId
30323047
properties:
30333048
destinationDefinitionId:
30343049
$ref: "#/components/schemas/DestinationDefinitionId"
30353050
workspaceId:
30363051
$ref: "#/components/schemas/WorkspaceId"
3052+
redirectUrl:
3053+
description: When completing OAuth flow to gain an access token, some API sometimes requires to verify that the app re-send the redirectUrl that was used when consent was given.
3054+
type: string
30373055
queryParams:
30383056
description: The query parameters present in the redirect URL after a user granted consent e.g auth code
30393057
type: object

airbyte-integrations/connectors/source-bigquery/src/main/java/io/airbyte/integrations/source/bigquery/BigQuerySource.java

-6
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,6 @@ protected List<TableInfo<CommonField<StandardSQLTypeName>>> discoverInternal(Big
140140
return result;
141141
}
142142

143-
@Override
144-
protected List<TableInfo<CommonField<StandardSQLTypeName>>> discoverInternal(BigQueryDatabase database, String schema) throws Exception {
145-
// todo to be added
146-
return discoverInternal(database);
147-
}
148-
149143
@Override
150144
protected Map<String, List<String>> discoverPrimaryKeys(BigQueryDatabase database, List<TableInfo<CommonField<StandardSQLTypeName>>> tableInfos) {
151145
return Collections.emptyMap();

airbyte-oauth/build.gradle

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
plugins {
22
id "java-library"
3+
id 'airbyte-integration-test-java'
34
}
45

56
dependencies {
6-
7+
implementation project(':airbyte-config:models')
8+
implementation project(':airbyte-config:persistence')
9+
implementation project(':airbyte-json-validation')
10+
testImplementation project(':airbyte-oauth')
711
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2020 Airbyte
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package io.airbyte.oauth;
26+
27+
import io.airbyte.config.DestinationOAuthParameter;
28+
import io.airbyte.config.SourceOAuthParameter;
29+
import java.util.Comparator;
30+
import java.util.Optional;
31+
import java.util.UUID;
32+
import java.util.stream.Stream;
33+
34+
public class MoreOAuthParameters {
35+
36+
public static Optional<SourceOAuthParameter> getSourceOAuthParameter(
37+
Stream<SourceOAuthParameter> stream,
38+
UUID workspaceId,
39+
UUID sourceDefinitionId) {
40+
return stream
41+
.filter(p -> sourceDefinitionId.equals(p.getSourceDefinitionId()))
42+
.filter(p -> p.getWorkspaceId() == null || workspaceId.equals(p.getWorkspaceId()))
43+
// we prefer params specific to a workspace before global ones (ie workspace is null)
44+
.min(Comparator.comparing(SourceOAuthParameter::getWorkspaceId, Comparator.nullsLast(Comparator.naturalOrder()))
45+
.thenComparing(SourceOAuthParameter::getOauthParameterId));
46+
}
47+
48+
public static Optional<DestinationOAuthParameter> getDestinationOAuthParameter(
49+
Stream<DestinationOAuthParameter> stream,
50+
UUID workspaceId,
51+
UUID destinationDefinitionId) {
52+
return stream
53+
.filter(p -> destinationDefinitionId.equals(p.getDestinationDefinitionId()))
54+
.filter(p -> p.getWorkspaceId() == null || workspaceId.equals(p.getWorkspaceId()))
55+
// we prefer params specific to a workspace before global ones (ie workspace is null)
56+
.min(Comparator.comparing(DestinationOAuthParameter::getWorkspaceId, Comparator.nullsLast(Comparator.naturalOrder()))
57+
.thenComparing(DestinationOAuthParameter::getOauthParameterId));
58+
}
59+
60+
}

airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthFlowImplementation.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,21 @@
2424

2525
package io.airbyte.oauth;
2626

27+
import io.airbyte.config.persistence.ConfigNotFoundException;
28+
import java.io.IOException;
2729
import java.util.Map;
2830
import java.util.UUID;
2931

3032
public interface OAuthFlowImplementation {
3133

32-
String getConsentUrl();
34+
String getSourceConsentUrl(UUID workspaceId, UUID sourceDefinitionId, String redirectUrl) throws IOException, ConfigNotFoundException;
3335

34-
Map<String, Object> completeOAuth(UUID workspaceId, Map<String, Object> queryParams);
36+
String getDestinationConsentUrl(UUID workspaceId, UUID destinationDefinitionId, String redirectUrl) throws IOException, ConfigNotFoundException;
37+
38+
Map<String, Object> completeSourceOAuth(UUID workspaceId, UUID sourceDefinitionId, Map<String, Object> queryParams, String redirectUrl)
39+
throws IOException, ConfigNotFoundException;
40+
41+
Map<String, Object> completeDestinationOAuth(UUID workspaceId, UUID destinationDefinitionId, Map<String, Object> queryParams, String redirectUrl)
42+
throws IOException, ConfigNotFoundException;
3543

3644
}

airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,23 @@
2525
package io.airbyte.oauth;
2626

2727
import com.google.common.collect.ImmutableMap;
28+
import io.airbyte.config.persistence.ConfigRepository;
29+
import io.airbyte.oauth.google.GoogleAdsOauthFlow;
30+
import io.airbyte.oauth.google.GoogleAnalyticsOauthFlow;
2831
import java.util.Map;
2932

3033
public class OAuthImplementationFactory {
3134

32-
static final Map<String, OAuthFlowImplementation> OAUTH_FLOW_MAPPING =
33-
ImmutableMap.<String, OAuthFlowImplementation>builder()
34-
.build();
35+
private final Map<String, OAuthFlowImplementation> OAUTH_FLOW_MAPPING;
3536

36-
public static OAuthFlowImplementation create(String imageName) {
37+
public OAuthImplementationFactory(ConfigRepository configRepository) {
38+
OAUTH_FLOW_MAPPING = ImmutableMap.<String, OAuthFlowImplementation>builder()
39+
.put("airbyte/source-google-analytics-v4", new GoogleAnalyticsOauthFlow(configRepository))
40+
.put("airbyte/source-google-ads", new GoogleAdsOauthFlow(configRepository))
41+
.build();
42+
}
43+
44+
public OAuthFlowImplementation create(String imageName) {
3745
if (OAUTH_FLOW_MAPPING.containsKey(imageName)) {
3846
return OAUTH_FLOW_MAPPING.get(imageName);
3947
} else {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2020 Airbyte
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package io.airbyte.oauth.google;
26+
27+
import com.fasterxml.jackson.databind.JsonNode;
28+
import io.airbyte.config.persistence.ConfigRepository;
29+
30+
public class GoogleAdsOauthFlow extends GoogleOAuthFlow {
31+
32+
public GoogleAdsOauthFlow(ConfigRepository configRepository) {
33+
super(configRepository, "https://www.googleapis.com/auth/adwords");
34+
}
35+
36+
@Override
37+
protected String getClientIdUnsafe(JsonNode config) {
38+
// the config object containing client ID and secret is nested inside the "credentials" object
39+
return super.getClientIdUnsafe(config.get("credentials"));
40+
}
41+
42+
@Override
43+
protected String getClientSecretUnsafe(JsonNode config) {
44+
// the config object containing client ID and secret is nested inside the "credentials" object
45+
return super.getClientSecretUnsafe(config.get("credentials"));
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2020 Airbyte
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package io.airbyte.oauth.google;
26+
27+
import io.airbyte.config.persistence.ConfigRepository;
28+
29+
public class GoogleAnalyticsOauthFlow extends GoogleOAuthFlow {
30+
31+
public GoogleAnalyticsOauthFlow(ConfigRepository configRepository) {
32+
super(configRepository, "https://www.googleapis.com/auth/analytics.readonly");
33+
}
34+
35+
}

0 commit comments

Comments
 (0)