🎉 Source greenhouse: added identification of accessible streams for API keys with limited permissions (#6238)
* Added identification of accessible streams for API keys with limited permissions * added credentials for API key with limited permissions (for user stream only) * update docs and image version * updated error message * fixed formatting
This commit is contained in:
1
.github/workflows/publish-command.yml
vendored
1
.github/workflows/publish-command.yml
vendored
@@ -117,6 +117,7 @@ jobs:
|
||||
GOOGLE_SHEETS_TESTS_CREDS: ${{ secrets.GOOGLE_SHEETS_TESTS_CREDS }}
|
||||
GOOGLE_WORKSPACE_ADMIN_REPORTS_TEST_CREDS: ${{ secrets.GOOGLE_WORKSPACE_ADMIN_REPORTS_TEST_CREDS }}
|
||||
GREENHOUSE_TEST_CREDS: ${{ secrets.GREENHOUSE_TEST_CREDS }}
|
||||
GREENHOUSE_TEST_CREDS_LIMITED: ${{ secrets.GREENHOUSE_TEST_CREDS_LIMITED }}
|
||||
HARVEST_INTEGRATION_TESTS_CREDS: ${{ secrets.HARVEST_INTEGRATION_TESTS_CREDS }}
|
||||
HUBSPOT_INTEGRATION_TESTS_CREDS: ${{ secrets.HUBSPOT_INTEGRATION_TESTS_CREDS }}
|
||||
INSTAGRAM_INTEGRATION_TESTS_CREDS: ${{ secrets.INSTAGRAM_INTEGRATION_TESTS_CREDS }}
|
||||
|
||||
1
.github/workflows/test-command.yml
vendored
1
.github/workflows/test-command.yml
vendored
@@ -112,6 +112,7 @@ jobs:
|
||||
GOOGLE_SHEETS_TESTS_CREDS: ${{ secrets.GOOGLE_SHEETS_TESTS_CREDS }}
|
||||
GOOGLE_WORKSPACE_ADMIN_REPORTS_TEST_CREDS: ${{ secrets.GOOGLE_WORKSPACE_ADMIN_REPORTS_TEST_CREDS }}
|
||||
GREENHOUSE_TEST_CREDS: ${{ secrets.GREENHOUSE_TEST_CREDS }}
|
||||
GREENHOUSE_TEST_CREDS_LIMITED: ${{ secrets.GREENHOUSE_TEST_CREDS_LIMITED }}
|
||||
HARVEST_INTEGRATION_TESTS_CREDS: ${{ secrets.HARVEST_INTEGRATION_TESTS_CREDS }}
|
||||
HUBSPOT_INTEGRATION_TESTS_CREDS: ${{ secrets.HUBSPOT_INTEGRATION_TESTS_CREDS }}
|
||||
INSTAGRAM_INTEGRATION_TESTS_CREDS: ${{ secrets.INSTAGRAM_INTEGRATION_TESTS_CREDS }}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"sourceDefinitionId": "59f1e50a-331f-4f09-b3e8-2e8d4d355f44",
|
||||
"name": "Greenhouse",
|
||||
"dockerRepository": "airbyte/source-greenhouse",
|
||||
"dockerImageTag": "0.2.3",
|
||||
"dockerImageTag": "0.2.4",
|
||||
"documentationUrl": "https://docs.airbyte.io/integrations/sources/greenhouse",
|
||||
"icon": "greenhouse.svg"
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@
|
||||
- sourceDefinitionId: 59f1e50a-331f-4f09-b3e8-2e8d4d355f44
|
||||
name: Greenhouse
|
||||
dockerRepository: airbyte/source-greenhouse
|
||||
dockerImageTag: 0.2.3
|
||||
dockerImageTag: 0.2.4
|
||||
documentationUrl: https://docs.airbyte.io/integrations/sources/greenhouse
|
||||
icon: greenhouse.svg
|
||||
sourceType: api
|
||||
|
||||
@@ -14,5 +14,5 @@ RUN pip install .
|
||||
|
||||
ENV AIRBYTE_ENTRYPOINT "/airbyte/base.sh"
|
||||
|
||||
LABEL io.airbyte.version=0.2.3
|
||||
LABEL io.airbyte.version=0.2.4
|
||||
LABEL io.airbyte.name=airbyte/source-greenhouse
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# See [Source Acceptance Tests](https://docs.airbyte.io/connector-development/testing-connectors/source-acceptance-tests-reference)
|
||||
# for more information about how to configure these tests
|
||||
connector_image: airbyte/source-greenhouse:dev
|
||||
tests:
|
||||
spec:
|
||||
- spec_path: "source_greenhouse/spec.json"
|
||||
connection:
|
||||
- config_path: "secrets/config.json"
|
||||
status: "succeed"
|
||||
- config_path: "secrets/config_users_only.json"
|
||||
status: "succeed"
|
||||
- config_path: "integration_tests/config_invalid.json"
|
||||
status: "failed"
|
||||
discovery:
|
||||
- config_path: "secrets/config.json"
|
||||
- config_path: "secrets/config_users_only.json"
|
||||
basic_read:
|
||||
- config_path: "secrets/config.json"
|
||||
configured_catalog_path: "integration_tests/configured_catalog.json"
|
||||
- config_path: "secrets/config.json"
|
||||
configured_catalog_path: "integration_tests/configured_catalog_users_only.json"
|
||||
full_refresh:
|
||||
- config_path: "secrets/config.json"
|
||||
configured_catalog_path: "integration_tests/configured_catalog.json"
|
||||
- config_path: "secrets/config_users_only.json"
|
||||
configured_catalog_path: "integration_tests/configured_catalog_users_only.json"
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
# Build latest connector image
|
||||
docker build . -t $(cat acceptance-test-config.yml | grep "connector_image" | head -n 1 | cut -d: -f2)
|
||||
|
||||
# Pull latest acctest image
|
||||
docker pull airbyte/source-acceptance-test:latest
|
||||
|
||||
# Run
|
||||
docker run --rm -it \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /tmp:/tmp \
|
||||
-v $(pwd):/test_input \
|
||||
airbyte/source-acceptance-test \
|
||||
--acceptance-test-config /test_input
|
||||
|
||||
@@ -24,25 +24,11 @@
|
||||
|
||||
|
||||
import pytest
|
||||
from grnhse.exceptions import EndpointNotFound, HTTPError
|
||||
from source_greenhouse.client import Client
|
||||
|
||||
pytest_plugins = ("source_acceptance_test.plugin",)
|
||||
|
||||
|
||||
def test__heal_check_with_wrong_api_key():
|
||||
client = Client(api_key="wrong_key")
|
||||
alive, error = client.health_check()
|
||||
|
||||
assert not alive
|
||||
assert error == '401 {"message":"Invalid Basic Auth credentials"}'
|
||||
|
||||
|
||||
def test__custom_fields_with_wrong_api_key():
|
||||
client = Client(api_key="wrong_key")
|
||||
with pytest.raises(HTTPError, match='401 {"message":"Invalid Basic Auth credentials"}'):
|
||||
list(client.list("custom_fields"))
|
||||
|
||||
|
||||
def test_client_wrong_endpoint():
|
||||
client = Client(api_key="wrong_key")
|
||||
with pytest.raises(EndpointNotFound, match="unknown_endpoint"):
|
||||
next(client.list("unknown_endpoint"))
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def connector_setup():
|
||||
""" This fixture is a placeholder for external resources that acceptance test might require."""
|
||||
yield
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"api_key": "bla"
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"stream": {
|
||||
"name": "applications",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "candidates",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "close_reasons",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "degrees",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "departments",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "job_posts",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "jobs",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "offers",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "scorecards",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "users",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
},
|
||||
{
|
||||
"stream": {
|
||||
"name": "custom_fields",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"stream": {
|
||||
"name": "users",
|
||||
"json_schema": {},
|
||||
"supported_sync_modes": ["full_refresh"],
|
||||
"source_defined_cursor": false
|
||||
},
|
||||
"sync_mode": "full_refresh",
|
||||
"destination_sync_mode": "overwrite"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
# This file is autogenerated -- only edit if you know what you are doing. Use setup.py for declaring dependencies.
|
||||
-e ../../bases/airbyte-protocol
|
||||
-e ../../bases/base-python
|
||||
-e ../../bases/source-acceptance-test
|
||||
-e .
|
||||
|
||||
@@ -25,12 +25,19 @@
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
TEST_REQUIREMENTS = [
|
||||
"pytest~=6.1",
|
||||
"source-acceptance-test",
|
||||
]
|
||||
setup(
|
||||
name="source_greenhouse",
|
||||
description="Source implementation for Greenhouse.",
|
||||
author="Airbyte",
|
||||
author_email="contact@airbyte.io",
|
||||
packages=find_packages(),
|
||||
install_requires=["airbyte-protocol", "base-python", "six==1.15.0", "grnhse-api==0.1.1", "pytest==6.1.2"],
|
||||
install_requires=["airbyte-protocol", "base-python", "six==1.15.0", "grnhse-api==0.1.1"],
|
||||
package_data={"": ["*.json", "schemas/*.json"]},
|
||||
extras_require={
|
||||
"tests": TEST_REQUIREMENTS,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -24,9 +24,10 @@
|
||||
|
||||
|
||||
from functools import partial
|
||||
from typing import Mapping, Tuple
|
||||
from typing import Generator, List, Mapping, Tuple
|
||||
|
||||
from base_python import BaseClient
|
||||
from airbyte_protocol import AirbyteStream
|
||||
from base_python import AirbyteLogger, BaseClient
|
||||
from grnhse import Harvest
|
||||
from grnhse.exceptions import HTTPError
|
||||
|
||||
@@ -67,16 +68,40 @@ class Client(BaseClient):
|
||||
def _enumerate_methods(self) -> Mapping[str, callable]:
|
||||
return {entity: partial(self.list, name=entity) for entity in self.ENTITIES}
|
||||
|
||||
def get_accessible_endpoints(self) -> List[str]:
|
||||
"""Try to read each supported endpoint and return accessible stream names"""
|
||||
logger = AirbyteLogger()
|
||||
accessible_endpoints = []
|
||||
for entity in self.ENTITIES:
|
||||
try:
|
||||
getattr(self._client, entity).get()
|
||||
accessible_endpoints.append(entity)
|
||||
except HTTPError as error:
|
||||
logger.warn(f"Endpoint '{entity}' error: {str(error)}")
|
||||
if "This API Key does not have permission for this endpoint" not in str(error):
|
||||
raise error
|
||||
logger.info(f"API key has access to {len(accessible_endpoints)} endpoints: {accessible_endpoints}")
|
||||
return accessible_endpoints
|
||||
|
||||
def health_check(self) -> Tuple[bool, str]:
|
||||
alive = True
|
||||
error_msg = None
|
||||
|
||||
try:
|
||||
# because there is no good candidate to try our connection
|
||||
# we use users endpoint as potentially smallest dataset
|
||||
self._client.users.get()
|
||||
accessible_endpoints = self.get_accessible_endpoints()
|
||||
if not accessible_endpoints:
|
||||
alive = False
|
||||
error_msg = "Your API Key does not have permission for any existing endpoints. Please grant read permissions for required streams/endpoints"
|
||||
|
||||
except HTTPError as error:
|
||||
alive = False
|
||||
error_msg = str(error)
|
||||
|
||||
return alive, error_msg
|
||||
|
||||
@property
|
||||
def streams(self) -> Generator[AirbyteStream, None, None]:
|
||||
"""Process accessible streams only"""
|
||||
accessible_endpoints = self.get_accessible_endpoints()
|
||||
for stream in super().streams:
|
||||
if stream.name in accessible_endpoints:
|
||||
yield stream
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"prospect_owner": {
|
||||
"type": "object",
|
||||
"type": ["null", "object"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
@@ -69,7 +69,15 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"current_stage": {
|
||||
"type": ["null", "string"]
|
||||
"type": ["null", "object"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"credited_to": {
|
||||
"type": "object",
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"prospect_owner": {
|
||||
"type": "object",
|
||||
"type": ["null", "object"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
@@ -137,7 +137,15 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"current_stage": {
|
||||
"type": ["null", "string"]
|
||||
"type": ["null", "object"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"credited_to": {
|
||||
"type": "object",
|
||||
|
||||
@@ -3,43 +3,43 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
"type": ["null", "integer"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"primary_email_address": {
|
||||
"type": "string"
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"created_at": {
|
||||
"type": "string"
|
||||
"type": ["null", "string"]
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean"
|
||||
"type": ["null", "boolean"]
|
||||
},
|
||||
"site_admin": {
|
||||
"type": "boolean"
|
||||
"type": ["null", "boolean"]
|
||||
},
|
||||
"emails": {
|
||||
"type": "array",
|
||||
"type": ["null", "array"],
|
||||
"items": {
|
||||
"type": "string"
|
||||
"type": ["null", "string"]
|
||||
}
|
||||
},
|
||||
"employee_id": {
|
||||
"type": ["null", "integer"]
|
||||
},
|
||||
"linked_candidate_ids": {
|
||||
"type": "array"
|
||||
"type": ["null", "array"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +46,8 @@ The Greenhouse connector should not run into Greenhouse API limitations under no
|
||||
|
||||
Please follow the [Greenhouse documentation for generating an API key](https://developers.greenhouse.io/harvest.html#authentication).
|
||||
|
||||
## Changelog
|
||||
|
||||
| Version | Date | Pull Request | Subject |
|
||||
| :------ | :-------- | :----- | :------ |
|
||||
| 0.2.4 | 2021-09-15 | [6238](https://github.com/airbytehq/airbyte/pull/6238) | added identification of accessible streams for API keys with limited permissions |
|
||||
|
||||
@@ -85,6 +85,7 @@ write_standard_creds source-google-search-console-singer "$GOOGLE_SEARCH_CONSOLE
|
||||
write_standard_creds source-google-sheets "$GOOGLE_SHEETS_TESTS_CREDS"
|
||||
write_standard_creds source-google-workspace-admin-reports "$GOOGLE_WORKSPACE_ADMIN_REPORTS_TEST_CREDS"
|
||||
write_standard_creds source-greenhouse "$GREENHOUSE_TEST_CREDS"
|
||||
write_standard_creds source-greenhouse "$GREENHOUSE_TEST_CREDS_LIMITED" "config_users_only.json"
|
||||
write_standard_creds source-harvest "$HARVEST_INTEGRATION_TESTS_CREDS"
|
||||
write_standard_creds source-hubspot "$HUBSPOT_INTEGRATION_TESTS_CREDS"
|
||||
write_standard_creds source-instagram "$INSTAGRAM_INTEGRATION_TESTS_CREDS"
|
||||
|
||||
Reference in New Issue
Block a user