-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
🎉 Source Google Analytics v4: Declare oauth parameters in google sources #6414
Changes from 6 commits
33817ac
9ba6acd
27c982e
ef16d8e
a6ba275
72afb01
0b69de5
ded60d2
277b763
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -383,55 +383,29 @@ def get_updated_state(self, current_stream_state: MutableMapping[str, Any], late | |||||||
return {self.cursor_field: max(latest_record.get(self.cursor_field, ""), current_stream_state.get(self.cursor_field, ""))} | ||||||||
|
||||||||
|
||||||||
class GoogleAnalyticsOauth2Authenticator(Oauth2Authenticator): | ||||||||
""" | ||||||||
This class supports either default authorization_code and JWT OAuth | ||||||||
authorizations in case of service account. | ||||||||
|
||||||||
Request example for API token extraction: | ||||||||
class GoogleAnalyticsServiceOauth2Authenticator(Oauth2Authenticator): | ||||||||
"""Request example for API token extraction: | ||||||||
curl --location --request POST | ||||||||
https://oauth2.googleapis.com/token?grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=signed_JWT | ||||||||
""" | ||||||||
|
||||||||
use_jwt_auth: bool = False | ||||||||
|
||||||||
def __init__(self, config): | ||||||||
client_secret, client_id, refresh_token = None, None, None | ||||||||
if "credentials_json" in config: | ||||||||
# Backward compatability with previous config format. Use | ||||||||
# credentials_json from config root. | ||||||||
auth = config | ||||||||
else: | ||||||||
auth = config["credentials"] | ||||||||
if "credentials_json" in auth: | ||||||||
# Service account JWT authorization | ||||||||
self.use_jwt_auth = True | ||||||||
credentials_json = json.loads(auth["credentials_json"]) | ||||||||
client_secret, client_id, refresh_token = credentials_json["private_key"], credentials_json["private_key_id"], None | ||||||||
self.client_email = credentials_json["client_email"] | ||||||||
else: | ||||||||
# OAuth 2.0 authorization_code authorization | ||||||||
client_secret, client_id, refresh_token = auth["client_secret"], auth["client_id"], auth["refresh_token"] | ||||||||
self.credentials_json = json.loads(config["credentials_json"]) | ||||||||
self.client_email = self.credentials_json["client_email"] | ||||||||
self.scope = "https://www.googleapis.com/auth/analytics.readonly" | ||||||||
|
||||||||
super().__init__( | ||||||||
token_refresh_endpoint="https://oauth2.googleapis.com/token", | ||||||||
client_secret=client_secret, | ||||||||
client_id=client_id, | ||||||||
refresh_token=refresh_token, | ||||||||
scopes=[self.scope], | ||||||||
client_secret=self.credentials_json["private_key"], | ||||||||
client_id=self.credentials_json["private_key_id"], | ||||||||
refresh_token=None, | ||||||||
) | ||||||||
|
||||||||
def refresh_access_token(self) -> Tuple[str, int]: | ||||||||
""" | ||||||||
Calling the Google OAuth 2.0 token endpoint. Used for authorizing | ||||||||
with signed JWT if credentials_json provided by config. Otherwise use | ||||||||
default OAuth2.0 workflow. | ||||||||
:return tuple with access token and token's time-to-live. | ||||||||
Calling the Google OAuth 2.0 token endpoint. Used for authorizing signed JWT. | ||||||||
Returns tuple with access token and token's time-to-live | ||||||||
""" | ||||||||
if not self.use_jwt_auth: | ||||||||
return super().refresh_access_token() | ||||||||
|
||||||||
response_json = None | ||||||||
try: | ||||||||
response = requests.request(method="POST", url=self.token_refresh_endpoint, params=self.get_refresh_request_params()) | ||||||||
|
@@ -452,7 +426,6 @@ def refresh_access_token(self) -> Tuple[str, int]: | |||||||
def get_refresh_request_params(self) -> Mapping[str, any]: | ||||||||
""" | ||||||||
Sign the JWT with RSA-256 using the private key found in service account JSON file. | ||||||||
Not used with default OAuth2.0 authorization_code grant_type. | ||||||||
""" | ||||||||
token_lifetime = 3600 # token lifetime is 1 hour | ||||||||
|
||||||||
|
@@ -469,17 +442,35 @@ def get_refresh_request_params(self) -> Mapping[str, any]: | |||||||
} | ||||||||
headers = {"kid": self.client_id} | ||||||||
signed_jwt = jwt.encode(payload, self.client_secret, headers=headers, algorithm="RS256") | ||||||||
return {"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": signed_jwt} | ||||||||
return {"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": str(signed_jwt)} | ||||||||
|
||||||||
|
||||||||
class SourceGoogleAnalyticsV4(AbstractSource): | ||||||||
"""Google Analytics lets you analyze data about customer engagement with your website or application.""" | ||||||||
|
||||||||
@staticmethod | ||||||||
def get_authenticator(config): | ||||||||
# backwards compatibility, credentials_json used to be in the top level of the connector | ||||||||
if config.get("credentials_json"): | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
return GoogleAnalyticsServiceOauth2Authenticator(config) | ||||||||
|
||||||||
auth_params = config.get("credentials") | ||||||||
if auth_params.pop("auth_type") == "Service": | ||||||||
return GoogleAnalyticsServiceOauth2Authenticator(auth_params) | ||||||||
else: | ||||||||
return Oauth2Authenticator( | ||||||||
token_refresh_endpoint="https://oauth2.googleapis.com/token", | ||||||||
client_secret=auth_params.get("client_secret"), | ||||||||
client_id=auth_params.get("client_id"), | ||||||||
refresh_token=auth_params.get("refresh_token"), | ||||||||
scopes=["https://www.googleapis.com/auth/analytics.readonly"], | ||||||||
) | ||||||||
|
||||||||
def check_connection(self, logger, config) -> Tuple[bool, any]: | ||||||||
try: | ||||||||
url = f"{GoogleAnalyticsV4TypesList.url_base}" | ||||||||
|
||||||||
authenticator = GoogleAnalyticsOauth2Authenticator(config) | ||||||||
authenticator = self.get_authenticator(config) | ||||||||
|
||||||||
session = requests.get(url, headers=authenticator.get_auth_header()) | ||||||||
session.raise_for_status() | ||||||||
|
@@ -496,7 +487,7 @@ def check_connection(self, logger, config) -> Tuple[bool, any]: | |||||||
def streams(self, config: Mapping[str, Any]) -> List[Stream]: | ||||||||
streams: List[GoogleAnalyticsV4Stream] = [] | ||||||||
|
||||||||
authenticator = GoogleAnalyticsOauth2Authenticator(config) | ||||||||
authenticator = self.get_authenticator(config) | ||||||||
|
||||||||
config["authenticator"] = authenticator | ||||||||
|
||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,50 +4,9 @@ | |
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "Google Analytics V4 Spec", | ||
"type": "object", | ||
"required": ["credentials", "view_id", "start_date"], | ||
"required": ["view_id", "start_date"], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why remove credentials as required? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the old configuration does not contain a credential field and when we run the connector the following error appears There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah I see, good point. Thanks for the clarification |
||
"additionalProperties": true, | ||
"properties": { | ||
"credentials": { | ||
"title": "Authentication mechanism", | ||
"type": "object", | ||
"description": "Choose either OAuth2.0 flow or provide your own JWT credentials for service account", | ||
"oneOf": [ | ||
{ | ||
"type": "object", | ||
"title": "OAuth2.0 authorization", | ||
"properties": { | ||
"option_title": { | ||
"type": "string", | ||
"const": "Default OAuth2.0 authorization" | ||
}, | ||
"client_id": { "type": "string" }, | ||
"client_secret": { "type": "string", "airbyte_secret": true }, | ||
"refresh_token": { "type": "string", "airbyte_secret": true }, | ||
"access_token": { "type": "string", "airbyte_secret": true } | ||
}, | ||
"required": ["client_id", "client_secret", "refresh_token"], | ||
"additionalProperties": false | ||
}, | ||
{ | ||
"type": "object", | ||
"title": "Service Account Key", | ||
"properties": { | ||
"option_title": { | ||
"type": "string", | ||
"const": "Service account credentials" | ||
}, | ||
"credentials_json": { | ||
"type": "string", | ||
"title": "Credentials JSON", | ||
"description": "The contents of the JSON service account key. Check out the <a href=\"https://docs.airbyte.io/integrations/sources/googleanalytics\">docs</a> if you need help generating this key.", | ||
"airbyte_secret": true | ||
} | ||
}, | ||
"required": ["credentials_json"], | ||
"additionalProperties": true | ||
} | ||
] | ||
}, | ||
"view_id": { | ||
"type": "string", | ||
"title": "View ID", | ||
|
@@ -70,6 +29,75 @@ | |
"title": "Custom Reports", | ||
"type": "string", | ||
"description": "A JSON array describing the custom reports you want to sync from GA. Check out the <a href=\"https://docs.airbyte.io/integrations/sources/google-analytics-v4\">docs</a> to get more information about this field." | ||
}, | ||
"credentials": { | ||
"type": "object", | ||
"oneOf": [ | ||
{ | ||
"title": "Authenticate via Google (Oauth)", | ||
"type": "object", | ||
"required": [ | ||
"auth_type", | ||
"client_id", | ||
"client_secret", | ||
"refresh_token" | ||
], | ||
"properties": { | ||
"auth_type": { | ||
"type": "string", | ||
"const": "Client", | ||
"enum": ["Client"], | ||
"default": "Client", | ||
"order": 0 | ||
}, | ||
"client_id": { | ||
"title": "Client ID", | ||
"type": "string", | ||
"description": "The Client ID of your developer application", | ||
"airbyte_secret": true | ||
}, | ||
"client_secret": { | ||
"title": "Client Secret", | ||
"type": "string", | ||
"description": "The client secret of your developer application", | ||
"airbyte_secret": true | ||
}, | ||
"refresh_token": { | ||
"title": "Refresh Token", | ||
"type": "string", | ||
"description": "A refresh token generated using the above client ID and secret", | ||
"airbyte_secret": true | ||
}, | ||
"access_token": { | ||
"title": "Access Token", | ||
"type": "string", | ||
"description": "A access token generated using the above client ID, secret and refresh_token", | ||
"airbyte_secret": true | ||
} | ||
} | ||
}, | ||
{ | ||
"type": "object", | ||
"title": "Service Account Key Authentication", | ||
"required": ["auth_type", "credentials_json"], | ||
"properties": { | ||
"auth_type": { | ||
"type": "string", | ||
"const": "Service", | ||
"enum": ["Service"], | ||
"default": "Service", | ||
"order": 0 | ||
}, | ||
"credentials_json": { | ||
"type": "string", | ||
"description": "The JSON key of the service account to use for authorization", | ||
"examples": [ | ||
"{ \"type\": \"service_account\", \"project_id\": YOUR_PROJECT_ID, \"private_key_id\": YOUR_PRIVATE_KEY, ... }" | ||
] | ||
} | ||
} | ||
} | ||
] | ||
} | ||
} | ||
}, | ||
|
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.
Since line 432 was removed, it doesn't seem to be using the method from super() in non-jwt cases... (ie when not using service account json?)
In those cases, it seems the grant_type should be simply a
refresh_token
value:airbyte/airbyte-cdk/python/airbyte_cdk/sources/streams/http/auth/oauth.py
Line 59 in f88b831
Is this making the connector supporting only the service account / jwt?
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.
The get_authenticator method contains the logic for choosing the authorization type, there is support for client and service authorization