Skip to content
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 SmartSheets: fix oauth2 implementation #23237

Merged
merged 12 commits into from
Mar 3, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ COPY $CODE_PATH ./$CODE_PATH
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]

LABEL io.airbyte.version=0.1.14
LABEL io.airbyte.version=1.0.0
LABEL io.airbyte.name=airbyte/source-smartsheets
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ connector_image: airbyte/source-smartsheets:dev
tests:
spec:
- spec_path: "source_smartsheets/spec.json"
backward_compatibility_tests_config:
disable_for_version: "0.1.14"
evantahler marked this conversation as resolved.
Show resolved Hide resolved
connection:
- config_path: "secrets/config.json"
status: "succeed"
- config_path: "secrets/config_oauth.json"
status: "succeed"
- config_path: "integration_tests/invalid_config.json"
status: "failed"
discovery:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,41 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#

import hashlib
import logging
from functools import cached_property
from typing import Any, Dict, Iterable, Mapping, Optional, Tuple

import smartsheet
from airbyte_cdk.sources.streams.http.requests_native_auth import SingleUseRefreshTokenOauth2Authenticator


class SmartSheetAPIWrapper:
def __init__(self, config: Mapping[str, Any]):
self._spreadsheet_id = config["spreadsheet_id"]
self._access_token = config["access_token"]
api_client = smartsheet.Smartsheet(self._access_token)
api_client.errors_as_exceptions(True)
self._config = config
self.api_client = smartsheet.Smartsheet(self.get_access_token(config))
self.api_client.errors_as_exceptions(True)
# each call to `Sheets` makes a new instance, so we save it here to make no more new objects
self._get_sheet = api_client.Sheets.get_sheet
self._get_sheet = self.api_client.Sheets.get_sheet
self._data = None

def get_token_hash(self, config: Mapping[str, Any]):
credentials = config.get("credentials")
return {"hash": hashlib.sha256(f"{credentials.get('client_secret')}|{credentials.get('refresh_token')}".encode()).hexdigest()}

def get_access_token(self, config: Mapping[str, Any]):
credentials = config.get("credentials")
if config.get("credentials", {}).get("auth_type") == "oauth2.0":
authenticator = SingleUseRefreshTokenOauth2Authenticator(
config, token_refresh_endpoint="https://api.smartsheet.com/2.0/token", refresh_request_body=self.get_token_hash(config)
)
return authenticator.get_access_token()

else:
access_token = credentials.get("access_token")
return access_token

def _fetch_sheet(self, from_dt: Optional[str] = None) -> None:
kwargs = {"rows_modified_since": from_dt}
if not from_dt:
Expand All @@ -43,6 +61,7 @@ def _construct_record(self, row: smartsheet.models.Row) -> Dict[str, str]:
@property
def data(self) -> smartsheet.models.Row:
if not self._data:
self.api_client._access_token = self.get_access_token(self._config)
self._fetch_sheet()
return self._data

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,68 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Smartsheets Source Spec",
"type": "object",
"required": ["access_token", "spreadsheet_id"],
"required": ["credentials", "spreadsheet_id"],
"additionalProperties": true,
"properties": {
"access_token": {
"title": "Access Token",
"description": "The access token to use for accessing your data from Smartsheets. This access token must be generated by a user with at least read access to the data you'd like to replicate. Generate an access token in the Smartsheets main menu by clicking Account > Apps & Integrations > API Access. See the <a href=\"https://docs.airbyte.com/integrations/sources/amplitude#setup-guide\">setup guide</a> for information on how to obtain this token.",
"type": "string",
"credentials": {
"title": "Authorization Method",
"type": "object",
"order": 0,
"airbyte_secret": true
"oneOf": [
{
"type": "object",
"title": "OAuth2.0",
"required": ["client_id", "client_secret", "refresh_token", "access_token", "token_expiry_date"],
"properties": {
"auth_type": {
"type": "string",
"const": "oauth2.0"
},
"client_id": {
"type": "string",
"description": "The API ID of the SmartSheets developer application.",
"airbyte_secret": true
},
"client_secret": {
"type": "string",
"description": "The API Secret the SmartSheets developer application.",
"airbyte_secret": true
},
"access_token": {
"type": "string",
"description": "Access Token for making authenticated requests.",
"airbyte_secret": true
},
"token_expiry_date": {
"type": "string",
"description": "The date-time when the access token should be refreshed.",
"format": "date-time"
},
"refresh_token": {
"type": "string",
"description": "The key to refresh the expired access_token.",
"airbyte_secret": true
}
}
},
{
"title": "API Access Token",
"type": "object",
"required": ["access_token"],
"properties": {
"auth_type": {
"type": "string",
"const": "access_token"
},
"access_token": {
"type": "string",
"title": "Access Token",
"description": "The access token to use for accessing your data from Smartsheets. This access token must be generated by a user with at least read access to the data you'd like to replicate. Generate an access token in the Smartsheets main menu by clicking Account > Apps & Integrations > API Access. See the <a href=\"https://docs.airbyte.com/integrations/sources/smartsheets/#setup-guide\">setup guide</a> for information on how to obtain this token.",
"airbyte_secret": true
}
}
}
]
},
"spreadsheet_id": {
"title": "Sheet ID",
Expand All @@ -34,16 +87,24 @@
},
"advanced_auth": {
"auth_flow_type": "oauth2.0",
"predicate_key": [],
"predicate_value": "",
"predicate_key": ["credentials", "auth_type"],
"predicate_value": "oauth2.0",
"oauth_config_specification": {
"complete_oauth_output_specification": {
"type": "object",
"additionalProperties": false,
"properties": {
"access_token": {
"type": "string",
"path_in_connector_config": ["access_token"]
"path_in_connector_config": ["credentials", "access_token"]
},
"refresh_token": {
"type": "string",
"path_in_connector_config": ["credentials", "refresh_token"]
},
"token_expiry_date": {
"type": "string",
"format": "date-time",
"path_in_connector_config": ["credentials", "token_expiry_date"]
}
}
},
Expand All @@ -61,8 +122,16 @@
},
"complete_oauth_server_output_specification": {
"type": "object",
"additionalProperties": false,
"properties": {}
"properties": {
"client_id": {
"type": "string",
"path_in_connector_config": ["credentials", "client_id"]
},
"client_secret": {
"type": "string",
"path_in_connector_config": ["credentials", "client_secret"]
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def response_mock():

@pytest.fixture
def config():
return {"spreadsheet_id": "id", "access_token": "token"}
return {"spreadsheet_id": "id", "credentials": {"access_token": "token"}}


@pytest.fixture
Expand Down
1 change: 1 addition & 0 deletions docs/integrations/sources/smartsheets.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ The remaining column datatypes supported by Smartsheets are more complex types (

| Version | Date | Pull Request | Subject |
|:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------------|
| 1.0.0 | 2023-02-19 | [23237](https://github.com/airbytehq/airbyte/pull/23237) | Fix OAuth2.0 token refresh |
| 0.1.14 | 2023-02-07 | [22419](https://github.com/airbytehq/airbyte/pull/22419) | OAuth2.0 support - enabled; add allowed hosts |
| 0.1.13 | 2022-12-02 | [20017](https://github.com/airbytehq/airbyte/pull/20017) | OAuth2.0 support - disabled |
| 0.1.12 | 2022-04-30 | [12500](https://github.com/airbytehq/airbyte/pull/12500) | Improve input configuration copy |
Expand Down