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 f13da31cbdacc6..35dd94329d316e 100644 --- a/airbyte-config/init/src/main/resources/seed/source_definitions.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_definitions.yaml @@ -620,7 +620,7 @@ - name: Okta sourceDefinitionId: 1d4fdb25-64fc-4569-92da-fcdca79a8372 dockerRepository: airbyte/source-okta - dockerImageTag: 0.1.7 + dockerImageTag: 0.1.8 documentationUrl: https://docs.airbyte.io/integrations/sources/okta icon: okta.svg sourceType: api diff --git a/airbyte-config/init/src/main/resources/seed/source_specs.yaml b/airbyte-config/init/src/main/resources/seed/source_specs.yaml index d9b9d43b439307..b4db3d4ec3c2c9 100644 --- a/airbyte-config/init/src/main/resources/seed/source_specs.yaml +++ b/airbyte-config/init/src/main/resources/seed/source_specs.yaml @@ -6030,32 +6030,119 @@ - - "client_secret" oauthFlowOutputParameters: - - "access_token" -- dockerImage: "airbyte/source-okta:0.1.7" +- dockerImage: "airbyte/source-okta:0.1.8" spec: documentationUrl: "https://docs.airbyte.io/integrations/sources/okta" connectionSpecification: $schema: "http://json-schema.org/draft-07/schema#" title: "Okta Spec" type: "object" - required: - - "token" - - "base_url" - additionalProperties: false + required: [] + additionalProperties: true properties: - token: - type: "string" - title: "API Token" - description: "A Okta token. See the docs for instructions on how to generate it." - airbyte_secret: true - base_url: + domain: type: "string" - title: "Base URL" - description: "The Okta base URL." - airbyte_secret: true + title: "Okta domain" + description: "The Okta domain. See the docs for instructions on how to find it." + airbyte_secret: false + credentials: + title: "Authorization Method *" + type: "object" + oneOf: + - type: "object" + title: "OAuth2.0" + required: + - "auth_type" + - "client_id" + - "client_secret" + - "refresh_token" + properties: + auth_type: + type: "string" + const: "oauth2.0" + order: 0 + client_id: + type: "string" + title: "Client ID" + description: "The Client ID of your OAuth application." + airbyte_secret: true + client_secret: + type: "string" + title: "Client Secret" + description: "The Client Secret of your OAuth application." + airbyte_secret: true + refresh_token: + type: "string" + title: "Refresh Token" + description: "Refresh Token to obtain new Access Token, when it's\ + \ expired." + airbyte_secret: true + - type: "object" + title: "API Token" + required: + - "auth_type" + - "api_token" + properties: + auth_type: + type: "string" + const: "api_token" + order: 0 + api_token: + type: "string" + title: "Personal API Token" + description: "An Okta token. See the docs for instructions on how to generate it." + airbyte_secret: true supportsNormalization: false supportsDBT: false supported_destination_sync_modes: [] + advanced_auth: + auth_flow_type: "oauth2.0" + predicate_key: + - "credentials" + - "auth_type" + predicate_value: "oauth2.0" + oauth_config_specification: + oauth_user_input_from_connector_config_specification: + type: "object" + additionalProperties: false + properties: + domain: + type: "string" + path_in_connector_config: + - "domain" + complete_oauth_output_specification: + type: "object" + additionalProperties: false + properties: + refresh_token: + type: "string" + path_in_connector_config: + - "credentials" + - "refresh_token" + complete_oauth_server_input_specification: + type: "object" + additionalProperties: false + properties: + client_id: + type: "string" + client_secret: + type: "string" + complete_oauth_server_output_specification: + type: "object" + additionalProperties: false + properties: + client_id: + type: "string" + path_in_connector_config: + - "credentials" + - "client_id" + client_secret: + type: "string" + path_in_connector_config: + - "credentials" + - "client_secret" - dockerImage: "airbyte/source-onesignal:0.1.2" spec: documentationUrl: "https://docs.airbyte.io/integrations/sources/onesignal" diff --git a/airbyte-integrations/connectors/source-okta/Dockerfile b/airbyte-integrations/connectors/source-okta/Dockerfile index ed4dd1de885d31..a853344ee6847a 100644 --- a/airbyte-integrations/connectors/source-okta/Dockerfile +++ b/airbyte-integrations/connectors/source-okta/Dockerfile @@ -12,5 +12,5 @@ RUN pip install . ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py" ENTRYPOINT ["python", "/airbyte/integration_code/main.py"] -LABEL io.airbyte.version=0.1.7 +LABEL io.airbyte.version=0.1.8 LABEL io.airbyte.name=airbyte/source-okta diff --git a/airbyte-integrations/connectors/source-okta/acceptance-test-config.yml b/airbyte-integrations/connectors/source-okta/acceptance-test-config.yml index 3d55a1bccd41ac..8e4c9a87ab8489 100644 --- a/airbyte-integrations/connectors/source-okta/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-okta/acceptance-test-config.yml @@ -5,6 +5,10 @@ tests: connection: - config_path: "secrets/config.json" status: "succeed" + - config_path: "secrets/config_api_token.json" + status: "succeed" + - config_path: "secrets/config_oauth.json" + status: "succeed" - config_path: "integration_tests/invalid_config.json" status: "failed" discovery: @@ -12,6 +16,8 @@ tests: basic_read: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/configured_catalog.json" + - config_path: "secrets/config_api_token.json" + configured_catalog_path: "integration_tests/configured_catalog.json" full_refresh: - config_path: "secrets/config.json" configured_catalog_path: "integration_tests/configured_catalog.json" diff --git a/airbyte-integrations/connectors/source-okta/source_okta/schemas/logs.json b/airbyte-integrations/connectors/source-okta/source_okta/schemas/logs.json index e0b67c1d807308..069d60a6a7659a 100644 --- a/airbyte-integrations/connectors/source-okta/source_okta/schemas/logs.json +++ b/airbyte-integrations/connectors/source-okta/source_okta/schemas/logs.json @@ -5,10 +5,7 @@ "alternateId": { "type": ["string", "null"] }, - "detail": { - "additionalProperties": { - "type": ["object", "null"] - }, + "detailEntry": { "type": ["object", "null"] }, "displayName": { @@ -38,11 +35,10 @@ "type": ["string", "null"] }, "authenticationStep": { - "type": ["integer", "null"] + "type": ["integer"] }, "credentialProvider": { "enum": [ - "OKTA_AUTHENTICATION_PROVIDER", "OKTA_CREDENTIAL_PROVIDER", "RSA", "SYMANTEC", @@ -63,6 +59,10 @@ "EMAIL", "OAUTH2", "JWT", + "CERTIFICATE", + "PRE_SHARED_SYMMETRIC_KEY", + "OKTA_CLIENT_SESSION", + "DEVICE_UDID", null ], "type": ["string", "null"] @@ -151,9 +151,6 @@ "debugContext": { "properties": { "debugData": { - "additionalProperties": { - "type": ["object", "null", "string"] - }, "type": ["object", "null"] } }, @@ -174,7 +171,16 @@ "type": ["string", "null"] }, "result": { - "type": ["string", "null"] + "enum": [ + "SUCCESS", + "FAILURE", + "SKIPPED", + "ALLOW", + "DENY", + "CHALLENGE", + "UNKNOWN" + ], + "type": "string" } }, "type": ["object", "null"] @@ -225,7 +231,7 @@ "type": ["string", "null"] }, "version": { - "type": "string" + "type": ["string", "null"] } }, "type": ["object", "null"] @@ -266,19 +272,16 @@ "type": ["string", "null"] }, "detailEntry": { - "additionalProperties": { - "type": ["object", "null", "string"] - }, "type": ["object", "null"] }, "displayName": { "type": ["string", "null"] }, "id": { - "type": ["string", "null"] + "type": "string" }, "type": { - "type": ["string", "null"] + "type": "string" } }, "type": ["object", "null"] @@ -288,9 +291,6 @@ "transaction": { "properties": { "detail": { - "additionalProperties": { - "type": ["object", "null", "string"] - }, "type": ["object", "null"] }, "id": { diff --git a/airbyte-integrations/connectors/source-okta/source_okta/source.py b/airbyte-integrations/connectors/source-okta/source_okta/source.py index 81ec19a6919722..bf3602dad2594e 100644 --- a/airbyte-integrations/connectors/source-okta/source_okta/source.py +++ b/airbyte-integrations/connectors/source-okta/source_okta/source.py @@ -13,7 +13,7 @@ from airbyte_cdk.sources import AbstractSource from airbyte_cdk.sources.streams import Stream from airbyte_cdk.sources.streams.http import HttpStream -from airbyte_cdk.sources.streams.http.auth import TokenAuthenticator +from airbyte_cdk.sources.streams.http.auth import Oauth2Authenticator, TokenAuthenticator class OktaStream(HttpStream, ABC): @@ -232,18 +232,67 @@ def path(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> str: return f"users/{user_id}/roles" +class OktaOauth2Authenticator(Oauth2Authenticator): + def get_refresh_request_body(self) -> Mapping[str, Any]: + return { + "grant_type": "refresh_token", + "refresh_token": self.refresh_token, + } + + def refresh_access_token(self) -> Tuple[str, int]: + try: + response = requests.request( + method="POST", + url=self.token_refresh_endpoint, + data=self.get_refresh_request_body(), + auth=(self.client_id, self.client_secret), + ) + response.raise_for_status() + response_json = response.json() + return response_json["access_token"], response_json["expires_in"] + except Exception as e: + raise Exception(f"Error while refreshing access token: {e}") from e + + class SourceOkta(AbstractSource): - def initialize_authenticator(self, config: Mapping[str, Any]) -> TokenAuthenticator: - return TokenAuthenticator(config["token"], auth_method="SSWS") + def initialize_authenticator(self, config: Mapping[str, Any]): + if "token" in config: + return TokenAuthenticator(config["token"], auth_method="SSWS") + + creds = config.get("credentials") + if not creds: + raise "Config validation error. `credentials` not specified." + + auth_type = creds.get("auth_type") + if not auth_type: + raise "Config validation error. `auth_type` not specified." + + if auth_type == "api_token": + return TokenAuthenticator(creds["api_token"], auth_method="SSWS") + + if auth_type == "oauth2.0": + return OktaOauth2Authenticator( + token_refresh_endpoint=self.get_token_refresh_endpoint(config), + client_secret=creds["client_secret"], + client_id=creds["client_id"], + refresh_token=creds["refresh_token"], + ) + + @staticmethod + def get_url_base(config: Mapping[str, Any]) -> str: + return config.get("base_url") or f"https://{config['domain']}.okta.com" + + def get_api_endpoint(self, config: Mapping[str, Any]) -> str: + return parse.urljoin(self.get_url_base(config), "/api/v1/") - def get_url_base(self, config: Mapping[str, Any]) -> str: - return parse.urljoin(config["base_url"], "/api/v1/") + def get_token_refresh_endpoint(self, config: Mapping[str, Any]) -> str: + return parse.urljoin(self.get_url_base(config), "/oauth2/v1/token") def check_connection(self, logger, config) -> Tuple[bool, any]: try: auth = self.initialize_authenticator(config) - base_url = self.get_url_base(config) - url = parse.urljoin(base_url, "users") + api_endpoint = self.get_api_endpoint(config) + url = parse.urljoin(api_endpoint, "users") response = requests.get( url, @@ -260,11 +309,11 @@ def check_connection(self, logger, config) -> Tuple[bool, any]: def streams(self, config: Mapping[str, Any]) -> List[Stream]: auth = self.initialize_authenticator(config) - url_base = self.get_url_base(config) + api_endpoint = self.get_api_endpoint(config) initialization_params = { "authenticator": auth, - "url_base": url_base, + "url_base": api_endpoint, } return [ diff --git a/airbyte-integrations/connectors/source-okta/source_okta/spec.json b/airbyte-integrations/connectors/source-okta/source_okta/spec.json index 8d83004b19583e..b258cbdc21af20 100644 --- a/airbyte-integrations/connectors/source-okta/source_okta/spec.json +++ b/airbyte-integrations/connectors/source-okta/source_okta/spec.json @@ -4,20 +4,126 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Okta Spec", "type": "object", - "required": ["token", "base_url"], - "additionalProperties": false, + "required": [], + "additionalProperties": true, "properties": { - "token": { + "domain": { "type": "string", - "title": "API Token", - "description": "A Okta token. See the docs for instructions on how to generate it.", - "airbyte_secret": true + "title": "Okta domain", + "description": "The Okta domain. See the docs for instructions on how to find it.", + "airbyte_secret": false }, - "base_url": { - "type": "string", - "title": "Base URL", - "description": "The Okta base URL.", - "airbyte_secret": true + "credentials": { + "title": "Authorization Method *", + "type": "object", + "oneOf": [ + { + "type": "object", + "title": "OAuth2.0", + "required": [ + "auth_type", + "client_id", + "client_secret", + "refresh_token" + ], + "properties": { + "auth_type": { + "type": "string", + "const": "oauth2.0", + "order": 0 + }, + "client_id": { + "type": "string", + "title": "Client ID", + "description": "The Client ID of your OAuth application.", + "airbyte_secret": true + }, + "client_secret": { + "type": "string", + "title": "Client Secret", + "description": "The Client Secret of your OAuth application.", + "airbyte_secret": true + }, + "refresh_token": { + "type": "string", + "title": "Refresh Token", + "description": "Refresh Token to obtain new Access Token, when it's expired.", + "airbyte_secret": true + } + } + }, + { + "type": "object", + "title": "API Token", + "required": ["auth_type", "api_token"], + "properties": { + "auth_type": { + "type": "string", + "const": "api_token", + "order": 0 + }, + "api_token": { + "type": "string", + "title": "Personal API Token", + "description": "An Okta token. See the docs for instructions on how to generate it.", + "airbyte_secret": true + } + } + } + ] + } + } + }, + "advanced_auth": { + "auth_flow_type": "oauth2.0", + "predicate_key": ["credentials", "auth_type"], + "predicate_value": "oauth2.0", + "oauth_config_specification": { + "complete_oauth_output_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "refresh_token": { + "type": "string", + "path_in_connector_config": ["credentials", "refresh_token"] + } + } + }, + "complete_oauth_server_input_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + } + } + }, + "complete_oauth_server_output_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "client_id": { + "type": "string", + "path_in_connector_config": ["credentials", "client_id"] + }, + "client_secret": { + "type": "string", + "path_in_connector_config": ["credentials", "client_secret"] + } + } + }, + "oauth_user_input_from_connector_config_specification": { + "type": "object", + "additionalProperties": false, + "properties": { + "domain": { + "type": "string", + "path_in_connector_config": ["domain"] + } + } } } } 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 b9484c77fd9d45..d80f0eaeade74e 100644 --- a/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/OAuthImplementationFactory.java @@ -62,6 +62,7 @@ public OAuthImplementationFactory(final ConfigRepository configRepository, final .put("airbyte/destination-snowflake", new DestinationSnowflakeOAuthFlow(configRepository, httpClient)) .put("airbyte/destination-google-sheets", new DestinationGoogleSheetsOAuthFlow(configRepository, httpClient)) .put("airbyte/source-snowflake", new SourceSnowflakeOAuthFlow(configRepository, httpClient)) + .put("airbyte/source-okta", new OktaOAuthFlow(configRepository, httpClient)) .build(); } diff --git a/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/OktaOAuthFlow.java b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/OktaOAuthFlow.java new file mode 100644 index 00000000000000..a877c682e76aec --- /dev/null +++ b/airbyte-oauth/src/main/java/io/airbyte/oauth/flows/OktaOAuthFlow.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.oauth.flows; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import io.airbyte.commons.json.Jsons; +import io.airbyte.config.persistence.ConfigRepository; +import io.airbyte.oauth.BaseOAuth2Flow; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; +import org.apache.http.client.utils.URIBuilder; + +/** + * Following docs from https://developer.okta.com/docs/guides/implement-oauth-for-okta/main/ + */ +public class OktaOAuthFlow extends BaseOAuth2Flow { + + public OktaOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient) { + super(configRepository, httpClient); + } + + @VisibleForTesting + public OktaOAuthFlow(final ConfigRepository configRepository, final HttpClient httpClient, final Supplier stateSupplier) { + super(configRepository, httpClient, stateSupplier); + } + + @Override + protected String formatConsentUrl(final UUID definitionId, + final String clientId, + final String redirectUrl, + final JsonNode inputOAuthConfiguration) + throws IOException { + + // getting domain value from user's config + final String domain = getConfigValueUnsafe(inputOAuthConfiguration, "domain"); + + final URIBuilder builder = new URIBuilder() + .setScheme("https") + .setHost(domain + ".okta.com") + .setPath("oauth2/v1/authorize") + // required + .addParameter("client_id", clientId) + .addParameter("redirect_uri", redirectUrl) + .addParameter("scope", "okta.users.read okta.logs.read okta.groups.read okta.roles.read offline_access") + .addParameter("response_type", "code") + .addParameter("state", getState()); + + try { + return builder.build().toString(); + } catch (final URISyntaxException e) { + throw new IOException("Failed to format Consent URL for OAuth flow", e); + } + } + + @Override + protected Map getAccessTokenQueryParameters(String clientId, + String clientSecret, + String authCode, + String redirectUrl) { + return ImmutableMap.builder() + // required + .put("code", authCode) + .put("redirect_uri", redirectUrl) + .put("grant_type", "authorization_code") + .build(); + } + + @Override + protected String getAccessTokenUrl(final JsonNode inputOAuthConfiguration) { + // getting domain value from user's config + final String domain = getConfigValueUnsafe(inputOAuthConfiguration, "domain"); + return "https://" + domain + ".okta.com/oauth2/v1/token"; + } + + @Override + protected Map completeOAuthFlow(final String clientId, + final String clientSecret, + final String authCode, + final String redirectUrl, + final JsonNode inputOAuthConfiguration, + final JsonNode oAuthParamConfig) + throws IOException { + final var accessTokenUrl = getAccessTokenUrl(inputOAuthConfiguration); + final byte[] authorization = Base64.getEncoder() + .encode((clientId + ":" + clientSecret).getBytes(StandardCharsets.UTF_8)); + final HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers + .ofString(tokenReqContentType.getConverter().apply( + getAccessTokenQueryParameters(clientId, clientSecret, authCode, redirectUrl)))) + .uri(URI.create(accessTokenUrl)) + .header("Content-Type", tokenReqContentType.getContentType()) + .header("Accept", "application/json") + .header("Authorization", "Basic " + new String(authorization, StandardCharsets.UTF_8)) + .build(); + try { + final HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString()); + + return extractOAuthOutput(Jsons.deserialize(response.body()), accessTokenUrl); + } catch (final InterruptedException e) { + throw new IOException("Failed to complete OAuth flow", e); + } + } + +} diff --git a/docs/integrations/sources/okta.md b/docs/integrations/sources/okta.md index 7ecf8d291fae72..aeec289651741b 100644 --- a/docs/integrations/sources/okta.md +++ b/docs/integrations/sources/okta.md @@ -41,35 +41,32 @@ The connector is restricted by normal Okta [requests limitation](https://develop ### Requirements -- Okta API Token +You can use [OAuth2.0](https://developer.okta.com/docs/guides/implement-grant-type/authcodepkce/main/) +or an [API token](https://developer.okta.com/docs/guides/create-an-api-token/overview/) to authenticate your Okta account. +If you choose to authenticate with OAuth2.0, [register](https://dev-01177082-admin.okta.com/admin/apps/active) your Okta application. ### Setup guide -In order to pull data out of your Okta instance, you need to create an [API Token](https://developer.okta.com/docs/guides/create-an-api-token/overview/). - -:::info +1. Use API token from requirements and Okta [domain](https://developer.okta.com/docs/guides/find-your-domain/-/main/). +2. Go to local Airbyte page. +3. In the left navigation bar, click **Sources**. In the top-right corner, click **+ new source**. +4. On the Set up the source page select **Okta** from the Source type dropdown. +5. Paste all data to required fields. +6. Click `Set up source`. +**Note:** Different Okta APIs require different admin privilege levels. API tokens inherit the privilege level of the admin account used to create them -::: - -1. Sign in to your Okta organization as a user with [administrator privileges](https://help.okta.com/en/prod/okta_help_CSH.htm#ext_Security_Administrators) -2. Access the API page: In the Admin Console, select API from the Security menu and then select the Tokens tab. -3. Click Create Token. -4. Name your token and click Create Token. -5. Record the token value. This is the only opportunity to see it and record it. -6. In Airbyte, create a Okta source. -7. You can now pull data from your Okta instance! - ## Changelog -| Version | Date | Pull Request | Subject | -| :------ | :--------- | :------------------------------------------------------- | :----------------------------------------------------------------------------- | +| Version | Date | Pull Request | Subject | +|:--------|:-----------| :--- | :--- | +| 0.1.8 | 2022-07-19 | [14710](https://github.com/airbytehq/airbyte/pull/14710) | Implement OAuth2.0 authorization method | | 0.1.7 | 2022-07-13 | [14556](https://github.com/airbytehq/airbyte/pull/14556) | add User_Role_Assignments and Group_Role_Assignments streams (full fetch only) | -| 0.1.6 | 2022-07-11 | [14610](https://github.com/airbytehq/airbyte/pull/14610) | add custom roles stream | -| 0.1.5 | 2022-07-04 | [14380](https://github.com/airbytehq/airbyte/pull/14380) | add Group_Members stream to okta source | -| 0.1.4 | 2021-11-02 | [7584](https://github.com/airbytehq/airbyte/pull/7584) | Fix incremental params for log stream | -| 0.1.3 | 2021-09-08 | [5905](https://github.com/airbytehq/airbyte/pull/5905) | Fix incremental stream defect | -| 0.1.2 | 2021-07-01 | [4456](https://github.com/airbytehq/airbyte/pull/4456) | Bugfix infinite pagination in logs stream | -| 0.1.1 | 2021-06-09 | [3937](https://github.com/airbytehq/airbyte/pull/3973) | Add `AIRBYTE_ENTRYPOINT` env variable for kubernetes support | -| 0.1.0 | 2021-05-30 | [3563](https://github.com/airbytehq/airbyte/pull/3563) | Initial Release | +| 0.1.6 | 2022-07-11 | [14610](https://github.com/airbytehq/airbyte/pull/14610) | add custom roles stream | +| 0.1.5 | 2022-07-04 | [14380](https://github.com/airbytehq/airbyte/pull/14380) | add Group_Members stream to okta source | +| 0.1.4 | 2021-11-02 | [7584](https://github.com/airbytehq/airbyte/pull/7584) | Fix incremental params for log stream | +| 0.1.3 | 2021-09-08 | [5905](https://github.com/airbytehq/airbyte/pull/5905) | Fix incremental stream defect | +| 0.1.2 | 2021-07-01 | [4456](https://github.com/airbytehq/airbyte/pull/4456) | Bugfix infinite pagination in logs stream | +| 0.1.1 | 2021-06-09 | [3937](https://github.com/airbytehq/airbyte/pull/3973) | Add `AIRBYTE_ENTRYPOINT` env variable for kubernetes support | +| 0.1.0 | 2021-05-30 | [3563](https://github.com/airbytehq/airbyte/pull/3563) | Initial Release |