Skip to content

Commit

Permalink
🪟🎉 Connector builder authentication (#20645)
Browse files Browse the repository at this point in the history
* allow auth configuration

* check for conflicts with the inferred inputs

* fix invisible inputs

* reduce redundancy and hide advanced input options for inferred inputs

* unnecessary validation

* typo

* unnecessary effect hook

* build spec even for invalid forms but do not update stream list

* fix keys

* 🪟🎉 Connector builder: Session token and oauth authentication (#20712)

* session token and oauth authentication

* fill in session token variable

* typos

* make sure validation error does not go away

* 🪟🎉 Connector builder: Always validate inputs form (#20664)

* validate user input outside of form

* review comments

Co-authored-by: lmossman <lake@airbyte.io>
  • Loading branch information
Joe Reuter and lmossman authored Dec 22, 2022
1 parent 8095fda commit 2bc3541
Show file tree
Hide file tree
Showing 20 changed files with 765 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
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 (
<BuilderCard>
<BuilderOneOf
path="global.authenticator"
label="Authentication"
tooltip="Authentication method to use for requests sent to the API"
options={[
{ label: "No Auth", typeValue: "NoAuth" },
{
label: "API Key",
typeValue: "ApiKeyAuthenticator",
default: {
api_token: "{{ config['api_key'] }}",
header: "",
},
children: (
<>
<BuilderField
type="string"
path="global.authenticator.header"
label="Header"
tooltip="HTTP header which should be set to the API Key"
/>
<UserInputField
label="API Key"
tooltip="The API key issued by the service. Fill it in in the user inputs"
/>
</>
),
},
{
label: "Bearer",
typeValue: "BearerAuthenticator",
default: {
api_token: "{{ config['api_key'] }}",
},
children: (
<UserInputField
label="API Key"
tooltip="The API key issued by the service. Fill it in in the user inputs"
/>
),
},
{
label: "Basic HTTP",
typeValue: "BasicHttpAuthenticator",
default: {
username: "{{ config['username'] }}",
password: "{{ config['password'] }}",
},
children: (
<>
<UserInputField label="Username" tooltip="The username for the login. Fill it in in the user inputs" />
<UserInputField label="Password" tooltip="The password for the login. Fill it in in the user inputs" />
</>
),
},
{
label: "OAuth",
typeValue: "OAuthAuthenticator",
default: {
client_id: "{{ config['client_id'] }}",
client_secret: "{{ config['client_secret'] }}",
refresh_token: "{{ config['client_refresh_token'] }}",
refresh_request_body: [],
token_refresh_endpoint: "",
},
children: (
<>
<BuilderField
type="string"
path="global.authenticator.token_refresh_endpoint"
label="Token refresh endpoint"
tooltip="The URL to call to obtain a new access token"
/>
<UserInputField label="Client ID" tooltip="The OAuth client ID" />
<UserInputField label="Client secret" tooltip="The OAuth client secret" />
<UserInputField label="Refresh token" tooltip="The OAuth refresh token" />
<BuilderOptional>
<BuilderField
type="array"
path="global.authenticator.scopes"
optional
label="Scopes"
tooltip="Scopes to request"
/>
<BuilderField
type="array"
path="global.authenticator.token_expiry_date_format"
optional
label="Token expiry date format"
tooltip="The format of the expiry date of the access token as obtained from the refresh endpoint"
/>
<BuilderField
type="string"
path="global.authenticator.expires_in_name"
optional
label="Token expiry property name"
tooltip="The name of the property which contains the token exipiry date in the response from the token refresh endpoint"
/>
<BuilderField
type="string"
path="global.authenticator.access_token_name"
optional
label="Access token property name"
tooltip="The name of the property which contains the access token in the response from the token refresh endpoint"
/>
<BuilderField
type="string"
path="global.authenticator.grant_type"
optional
label="Grant type"
tooltip="The grant type to request for access_token"
/>
<KeyValueListField
path="global.authenticator.refresh_request_body"
label="Request Parameters"
tooltip="The request body to send in the refresh request"
/>
</BuilderOptional>
</>
),
},
{
label: "Session token",
typeValue: "SessionTokenAuthenticator",
default: {
username: "{{ config['username'] }}",
password: "{{ config['password'] }}",
session_token: "{{ config['session_token'] }}",
},
children: (
<>
<BuilderField
type="string"
path="global.authenticator.header"
label="Header"
tooltip="Specific HTTP header of source API for providing session token"
/>
<BuilderField
type="string"
path="global.authenticator.session_token_response_key"
label="Session token response key"
tooltip="Key for retrieving session token from api response"
/>
<BuilderField
type="string"
path="global.authenticator.login_url"
label="Login url"
tooltip="Url for getting a specific session token"
/>
<BuilderField
type="string"
path="global.authenticator.validate_session_url"
label="Validate session url"
tooltip="Url to validate passed session token"
/>
<UserInputField label="Username" tooltip="The username" />
<UserInputField label="Password" tooltip="The password" />
<UserInputField
label="Session token"
tooltip="Session token generated by user (if provided username and password are not required)"
/>
</>
),
},
]}
/>
</BuilderCard>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useEffect } from "react";

import { BuilderView, useConnectorBuilderState } from "services/connectorBuilder/ConnectorBuilderStateService";

import { BuilderFormValues } from "../types";
import { builderFormValidationSchema, BuilderFormValues } from "../types";
import styles from "./Builder.module.scss";
import { BuilderSidebar } from "./BuilderSidebar";
import { GlobalConfigView } from "./GlobalConfigView";
Expand All @@ -29,7 +29,7 @@ function getView(selectedView: BuilderView) {
export const Builder: React.FC<BuilderProps> = ({ values, toggleYamlEditor }) => {
const { setBuilderFormValues, selectedView } = useConnectorBuilderState();
useEffect(() => {
setBuilderFormValues(values);
setBuilderFormValues(values, builderFormValidationSchema.isValidSync(values));
}, [values, setBuilderFormValues]);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useField } from "formik";
import React from "react";

import GroupControls from "components/GroupControls";
import { ControlLabels } from "components/LabeledControl";
import { DropDown } from "components/ui/DropDown";

interface Option {
label: string;
value: string;
default?: object;
}

interface OneOfOption {
label: string; // label shown in the dropdown menu
typeValue: string; // value to set on the `type` field for this component - should match the oneOf type definition
default?: object; // default values for the path
children?: React.ReactNode;
}

interface BuilderOneOfProps {
options: OneOfOption[];
path: string; // path to the oneOf component in the json schema
label: string;
tooltip: string;
}

export const BuilderOneOf: React.FC<BuilderOneOfProps> = ({ options, path, label, tooltip }) => {
const [, , oneOfPathHelpers] = useField(path);
const typePath = `${path}.type`;
const [typePathField] = useField(typePath);
const value = typePathField.value;

const selectedOption = options.find((option) => option.typeValue === value);

return (
<GroupControls
label={<ControlLabels label={label} infoTooltipContent={tooltip} />}
dropdown={
<DropDown
{...typePathField}
options={options.map((option) => {
return { label: option.label, value: option.typeValue, default: option.default };
})}
value={value ?? options[0].typeValue}
onChange={(selectedOption: Option) => {
if (selectedOption.value === value) {
return;
}
// clear all values for this oneOf and set selected option and default values
oneOfPathHelpers.setValue({
type: selectedOption.value,
...selectedOption.default,
});
}}
/>
}
>
{selectedOption?.children}
</GroupControls>
);
};
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<React.PropsWithChildren<unknown>> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className={styles.wrapper}>
<button
onClick={() => {
setIsOpen(!isOpen);
}}
className={classNames(styles.label, { [styles.closed]: !isOpen })}
>
{isOpen ? <FontAwesomeIcon icon={faAngleDown} /> : <FontAwesomeIcon icon={faAngleRight} />}
<FormattedMessage id="connectorBuilder.optionalFieldsLabel" />
</button>
{isOpen && <div className={styles.container}>{children}</div>}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "services/connectorBuilder/ConnectorBuilderStateService";

import { DownloadYamlButton } from "../DownloadYamlButton";
import { BuilderFormValues } from "../types";
import { BuilderFormValues, getInferredInputs } from "../types";
import { useBuilderErrors } from "../useBuilderErrors";
import { AddStreamButton } from "./AddStreamButton";
import styles from "./BuilderSidebar.module.scss";
Expand Down Expand Up @@ -115,7 +115,10 @@ export const BuilderSidebar: React.FC<BuilderSidebarProps> = ({ className, toggl
onClick={() => handleViewSelect("inputs")}
>
<FontAwesomeIcon icon={faUser} />
<FormattedMessage id="connectorBuilder.userInputs" values={{ number: values.inputs.length }} />
<FormattedMessage
id="connectorBuilder.userInputs"
values={{ number: values.inputs.length + getInferredInputs(values).length }}
/>
</ViewSelectButton>

<div className={styles.streamsHeader}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useIntl } from "react-intl";

import { AuthenticationSection } from "./AuthenticationSection";
import { BuilderCard } from "./BuilderCard";
import { BuilderConfigView } from "./BuilderConfigView";
import { BuilderField } from "./BuilderField";
Expand All @@ -16,6 +17,7 @@ export const GlobalConfigView: React.FC = () => {
<BuilderCard className={styles.content}>
<BuilderField type="string" path="global.urlBase" label="API URL" tooltip="Base URL of the source API" />
</BuilderCard>
<AuthenticationSection />
</BuilderConfigView>
);
};
Loading

0 comments on commit 2bc3541

Please sign in to comment.