From a17ab05a2a4fb5a6a61408878b0ad1c33ac2ef3b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 20 Dec 2022 14:58:11 +0100 Subject: [PATCH 1/4] session token and oauth authentication --- .../Builder/AuthenticationSection.tsx | 177 ++++++++++++++++++ .../Builder/BuilderOptional.module.scss | 34 ++++ .../Builder/BuilderOptional.tsx | 25 +++ .../Builder/GlobalConfigView.tsx | 68 +------ .../src/components/connectorBuilder/types.ts | 128 ++++++++++++- airbyte-webapp/src/locales/en.json | 1 + 6 files changed, 363 insertions(+), 70 deletions(-) create mode 100644 airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx create mode 100644 airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.module.scss create mode 100644 airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.tsx diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx new file mode 100644 index 0000000000000..d49037eb452b3 --- /dev/null +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx @@ -0,0 +1,177 @@ +import { BuilderCard } from "./BuilderCard"; +import { BuilderField } from "./BuilderField"; +import { BuilderOneOf } from "./BuilderOneOf"; +import { BuilderOptional } from "./BuilderOptional"; +import { KeyValueListField } from "./KeyValueListField"; +import { UserInputField } from "./UserInputField"; + +export const AuthenticationSection: React.FC = () => { + return ( + + + + + + ), + }, + { + label: "Bearer", + typeValue: "BearerAuthenticator", + default: { + api_token: "{{ config['api_key'] }}", + }, + children: ( + + ), + }, + { + label: "Basic HTTP", + typeValue: "BasicHttpAuthenticator", + default: { + username: "{{ config['username'] }}", + password: "{{ config['password'] }}", + }, + children: ( + <> + + + + ), + }, + { + label: "OAuth", + typeValue: "OAuthAuthenticator", + default: { + client_id: "{{ config['client_id'] }}", + client_secret: "{{ config['client_secret'] }}", + refresh_token: "{{ config['client_refresh_token'] }}", + refresh_request_body: [], + }, + children: ( + <> + + + + + + + + + + + + + + ), + }, + { + label: "Session token", + typeValue: "SessionTokenAuthenticator", + default: { + username: "{{ config['username'] }}", + password: "{{ config['password'] }}", + session_token: "", + }, + children: ( + <> + + + + + + + + + ), + }, + ]} + /> + + ); +}; diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.module.scss b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.module.scss new file mode 100644 index 0000000000000..b5a7816f6ae11 --- /dev/null +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.module.scss @@ -0,0 +1,34 @@ +@use "scss/variables"; +@use "scss/colors"; +@use "scss/mixins"; + +.wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + border-top: variables.$border-thin solid colors.$grey-100; + gap: variables.$spacing-lg; +} + +.container { + padding-left: variables.$spacing-xl; + display: flex; + flex-direction: column; + align-items: stretch; + align-self: stretch; + gap: variables.$spacing-lg; +} + +.label { + cursor: pointer; + background: none; + border: none; + display: flex; + gap: variables.$spacing-sm; + margin-top: variables.$spacing-lg; + align-items: center; + + &.closed { + color: colors.$grey-400; + } +} diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.tsx new file mode 100644 index 0000000000000..639f5358726be --- /dev/null +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/BuilderOptional.tsx @@ -0,0 +1,25 @@ +import { faAngleDown, faAngleRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import classNames from "classnames"; +import React, { useState } from "react"; +import { FormattedMessage } from "react-intl"; + +import styles from "./BuilderOptional.module.scss"; + +export const BuilderOptional: React.FC> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + return ( +
+ + {isOpen &&
{children}
} +
+ ); +}; diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx index 43c659f067971..bc8e29a532a55 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/GlobalConfigView.tsx @@ -1,12 +1,11 @@ import { useIntl } from "react-intl"; +import { AuthenticationSection } from "./AuthenticationSection"; import { BuilderCard } from "./BuilderCard"; import { BuilderConfigView } from "./BuilderConfigView"; import { BuilderField } from "./BuilderField"; -import { BuilderOneOf } from "./BuilderOneOf"; import { BuilderTitle } from "./BuilderTitle"; import styles from "./GlobalConfigView.module.scss"; -import { UserInputField } from "./UserInputField"; export const GlobalConfigView: React.FC = () => { const { formatMessage } = useIntl(); @@ -18,70 +17,7 @@ export const GlobalConfigView: React.FC = () => { - - - - - - ), - }, - { - label: "Bearer", - typeValue: "BearerAuthenticator", - default: { - api_token: "{{ config['api_key'] }}", - }, - children: ( - - ), - }, - { - label: "Basic HTTP", - typeValue: "BasicHttpAuthenticator", - default: { - username: "{{ config['username'] }}", - password: "{{ config['password'] }}", - }, - children: ( - <> - - - - ), - }, - ]} - /> - + ); }; diff --git a/airbyte-webapp/src/components/connectorBuilder/types.ts b/airbyte-webapp/src/components/connectorBuilder/types.ts index ea1af0428cff6..723f9853cf0e4 100644 --- a/airbyte-webapp/src/components/connectorBuilder/types.ts +++ b/airbyte-webapp/src/components/connectorBuilder/types.ts @@ -4,7 +4,16 @@ import * as yup from "yup"; import { SourceDefinitionSpecificationDraft } from "core/domain/connector"; import { PatchedConnectorManifest } from "core/domain/connectorBuilder/PatchedConnectorManifest"; import { AirbyteJSONSchema } from "core/jsonSchema/types"; -import { DeclarativeStream, HttpRequesterAllOfAuthenticator } from "core/request/ConnectorManifest"; +import { + ApiKeyAuthenticator, + BasicHttpAuthenticator, + BearerAuthenticator, + DeclarativeOauth2AuthenticatorAllOf, + DeclarativeStream, + HttpRequesterAllOfAuthenticator, + NoAuth, + SessionTokenAuthenticator, +} from "core/request/ConnectorManifest"; export interface BuilderFormInput { key: string; @@ -12,11 +21,22 @@ export interface BuilderFormInput { definition: AirbyteJSONSchema; } +type BuilderFormAuthenticator = ( + | NoAuth + | (Omit & { + refresh_request_body: Array<[string, string]>; + }) + | ApiKeyAuthenticator + | BearerAuthenticator + | BasicHttpAuthenticator + | SessionTokenAuthenticator +) & { type: string }; + export interface BuilderFormValues { global: { connectorName: string; urlBase: string; - authenticator: HttpRequesterAllOfAuthenticator; + authenticator: BuilderFormAuthenticator; }; inputs: BuilderFormInput[]; inferredInputOverrides: Record>; @@ -82,6 +102,68 @@ function getInferredInputList(values: BuilderFormValues): BuilderFormInput[] { }, ]; } + if (values.global.authenticator.type === "OAuthAuthenticator") { + return [ + { + key: "client_id", + required: true, + definition: { + type: "string", + title: "Client ID", + airbyte_secret: true, + }, + }, + { + key: "client_secret", + required: true, + definition: { + type: "string", + title: "Client secret", + airbyte_secret: true, + }, + }, + { + key: "refresh_token", + required: true, + definition: { + type: "string", + title: "Refresh token", + airbyte_secret: true, + }, + }, + ]; + } + if (values.global.authenticator.type === "SessionTokenAuthenticator") { + return [ + { + key: "username", + required: false, + definition: { + type: "string", + title: "Username", + }, + }, + { + key: "password", + required: false, + definition: { + type: "string", + title: "Password", + airbyte_secret: true, + }, + }, + { + key: "session_token", + required: false, + definition: { + type: "string", + title: "Session token", + description: "Session token generated by user (if provided username and password are not required)", + airbyte_secret: true, + }, + }, + ]; + } return []; } @@ -103,7 +185,7 @@ export const builderFormValidationSchema = yup.object().shape({ urlBase: yup.string().required("form.empty.error"), authenticator: yup.object({ header: yup.mixed().when("type", { - is: "ApiKeyAuthenticator", + is: (type: string) => type === "ApiKeyAuthenticator" || type === "SessionTokenAuthenticator", then: yup.string().required("form.empty.error"), otherwise: (schema) => schema.strip(), }), @@ -122,6 +204,26 @@ export const builderFormValidationSchema = yup.object().shape({ then: yup.string().required("form.empty.error"), otherwise: (schema) => schema.strip(), }), + token_refresh_endpoint: yup.mixed().when("type", { + is: "OAuthAuthenticator", + then: yup.string().required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), + session_token_response_key: yup.mixed().when("type", { + is: "SessionTokenAuthenticator", + then: yup.string().required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), + login_url: yup.mixed().when("type", { + is: "SessionTokenAuthenticator", + then: yup.string().required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), + validate_session_url: yup.mixed().when("type", { + is: "SessionTokenAuthenticator", + then: yup.string().required("form.empty.error"), + otherwise: (schema) => schema.strip(), + }), }), }), streams: yup.array().of( @@ -139,6 +241,24 @@ export const builderFormValidationSchema = yup.object().shape({ ), }); +function builderFormAuthenticatorToAuthenticator( + globalSettings: BuilderFormValues["global"] +): HttpRequesterAllOfAuthenticator { + if (globalSettings.authenticator.type === "OAuthAuthenticator") { + return { + ...globalSettings.authenticator, + refresh_request_body: Object.fromEntries(globalSettings.authenticator.refresh_request_body), + }; + } + if (globalSettings.authenticator.type === "SessionTokenAuthenticator") { + return { + ...globalSettings.authenticator, + api_url: globalSettings.urlBase, + }; + } + return globalSettings.authenticator as HttpRequesterAllOfAuthenticator; +} + export const convertToManifest = (values: BuilderFormValues): PatchedConnectorManifest => { const manifestStreams: DeclarativeStream[] = values.streams.map((stream) => { return { @@ -154,7 +274,7 @@ export const convertToManifest = (values: BuilderFormValues): PatchedConnectorMa request_headers: Object.fromEntries(stream.requestOptions.requestHeaders), request_body_data: Object.fromEntries(stream.requestOptions.requestBody), }, - authenticator: values.global.authenticator, + authenticator: builderFormAuthenticatorToAuthenticator(values.global), // TODO: remove these empty "config" values once they are no longer required in the connector manifest JSON schema config: {}, }, diff --git a/airbyte-webapp/src/locales/en.json b/airbyte-webapp/src/locales/en.json index df4641dbe32cf..47d946bc15b3c 100644 --- a/airbyte-webapp/src/locales/en.json +++ b/airbyte-webapp/src/locales/en.json @@ -716,6 +716,7 @@ "connectorBuilder.inputsNoSpecUITooltip": "Add User Input fields to allow setting test inputs", "connectorBuilder.inputsNoSpecYAMLTooltip": "Add a spec to your manifest to allow setting test inputs", "connectorBuilder.setInUserInput": "This setting is configured as part of the user inputs in the testing panel", + "connectorBuilder.optionalFieldsLabel": "Optional fields", "jobs.noAttemptsFailure": "Failed to start job.", From 7b6470e53b844f3242f554e2d8a061a1b15312c6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 20 Dec 2022 15:20:01 +0100 Subject: [PATCH 2/4] fill in session token variable --- .../connectorBuilder/Builder/AuthenticationSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx index d49037eb452b3..5872449cd93c2 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx @@ -133,7 +133,7 @@ export const AuthenticationSection: React.FC = () => { default: { username: "{{ config['username'] }}", password: "{{ config['password'] }}", - session_token: "", + session_token: "{{ config['session_token'] }}", }, children: ( <> From 4bd0bdcb8a22be86d08e9716a887ebfd17bb2824 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 21 Dec 2022 14:10:10 +0100 Subject: [PATCH 3/4] typos --- .../connectorBuilder/Builder/AuthenticationSection.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx index 74ac4e899f1fc..af3835d7e34f1 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx @@ -79,7 +79,7 @@ export const AuthenticationSection: React.FC = () => { label="Token refresh endpoint" tooltip="The URL to call to obtain a new access token" /> - + @@ -88,7 +88,7 @@ export const AuthenticationSection: React.FC = () => { path="global.authenticator.scopes" optional label="Scopes" - tooltip="Scopes to rquest" + tooltip="Scopes to request" /> { type="string" path="global.authenticator.header" label="Header" - tooltip="Specific header of source for providing session token" + tooltip="Specific HTTP header of source API for providing session token" /> { type="string" path="global.authenticator.login_url" label="Login url" - tooltip="Url fot getting a specific session token" + tooltip="Url for getting a specific session token" /> Date: Wed, 21 Dec 2022 14:28:26 +0100 Subject: [PATCH 4/4] make sure validation error does not go away --- .../connectorBuilder/Builder/AuthenticationSection.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx index af3835d7e34f1..89a06bcba7a65 100644 --- a/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx +++ b/airbyte-webapp/src/components/connectorBuilder/Builder/AuthenticationSection.tsx @@ -19,6 +19,7 @@ export const AuthenticationSection: React.FC = () => { typeValue: "ApiKeyAuthenticator", default: { api_token: "{{ config['api_key'] }}", + header: "", }, children: ( <> @@ -70,6 +71,7 @@ export const AuthenticationSection: React.FC = () => { client_secret: "{{ config['client_secret'] }}", refresh_token: "{{ config['client_refresh_token'] }}", refresh_request_body: [], + token_refresh_endpoint: "", }, children: ( <>