diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
index d5d09cec862..d2170de6f8f 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
@@ -161,23 +161,20 @@ export const httpPathField = ({
getValidation,
validationErrors,
db,
-}: FieldPropTypes) => {
- console.error(db);
- return (
-
- );
-};
+}: FieldPropTypes) => (
+
+);
export const usernameField = ({
required,
changeMethods,
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/OAuth2ClientField.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/OAuth2ClientField.tsx
new file mode 100644
index 00000000000..22be10d1d17
--- /dev/null
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/OAuth2ClientField.tsx
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { useState } from 'react';
+
+import Collapse from 'src/components/Collapse';
+import { Input } from 'src/components/Input';
+import { FormItem } from 'src/components/Form';
+import { FieldPropTypes } from '../../types';
+
+interface OAuth2ClientInfo {
+ id: string;
+ secret: string;
+ authorization_request_uri: string;
+ token_request_uri: string;
+ scope: string;
+}
+
+export const OAuth2ClientField = ({ changeMethods, db }: FieldPropTypes) => {
+ const encryptedExtra = JSON.parse(db?.masked_encrypted_extra || '{}');
+ const [oauth2ClientInfo, setOauth2ClientInfo] = useState({
+ id: encryptedExtra.oauth2_client_info?.id || '',
+ secret: encryptedExtra.oauth2_client_info?.secret || '',
+ authorization_request_uri:
+ encryptedExtra.oauth2_client_info?.authorization_request_uri || '',
+ token_request_uri:
+ encryptedExtra.oauth2_client_info?.token_request_uri || '',
+ scope: encryptedExtra.oauth2_client_info?.scope || '',
+ });
+
+ if (db?.engine_information?.supports_oauth2 !== true) {
+ return null;
+ }
+
+ const handleChange = (key: any) => (e: any) => {
+ const updatedInfo = {
+ ...oauth2ClientInfo,
+ [key]: e.target.value,
+ };
+
+ setOauth2ClientInfo(updatedInfo);
+
+ const event = {
+ target: {
+ name: 'oauth2_client_info',
+ value: updatedInfo,
+ },
+ };
+ changeMethods.onEncryptedExtraInputChange(event);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
index 545403a3610..0f96a9e00e8 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
@@ -41,6 +41,7 @@ import {
} from './CommonParameters';
import { validatedInputField } from './ValidatedInputField';
import { EncryptedField } from './EncryptedField';
+import { OAuth2ClientField } from './OAuth2ClientField';
import { TableCatalog } from './TableCatalog';
import { formScrollableStyles, validatedFormStyles } from '../styles';
import { DatabaseForm, DatabaseObject } from '../../types';
@@ -67,6 +68,7 @@ export const FormFieldOrder = [
'warehouse',
'role',
'ssh',
+ 'oauth2_client',
];
const extensionsRegistry = getExtensionsRegistry();
@@ -84,6 +86,7 @@ const FORM_FIELD_MAP = {
default_schema: defaultSchemaField,
username: usernameField,
password: passwordField,
+ oauth2_client: OAuth2ClientField,
access_token: accessTokenField,
database_name: displayField,
query: queryField,
@@ -118,6 +121,9 @@ interface DatabaseConnectionFormProps {
onExtraInputChange: (
event: FormEvent | { target: HTMLInputElement },
) => void;
+ onEncryptedExtraInputChange: (
+ event: FormEvent | { target: HTMLInputElement },
+ ) => void;
onAddTableCatalog: () => void;
onRemoveTableCatalog: (idx: number) => void;
validationErrors: JsonObject | null;
@@ -136,6 +142,7 @@ const DatabaseConnectionForm = ({
onAddTableCatalog,
onChange,
onExtraInputChange,
+ onEncryptedExtraInputChange,
onParametersChange,
onParametersUploadFileChange,
onQueryChange,
@@ -171,6 +178,7 @@ const DatabaseConnectionForm = ({
onAddTableCatalog,
onRemoveTableCatalog,
onExtraInputChange,
+ onEncryptedExtraInputChange,
},
validationErrors,
getValidation,
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index 48882131282..53215570a6a 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -154,6 +154,7 @@ export enum ActionType {
EditorChange,
ExtraEditorChange,
ExtraInputChange,
+ EncryptedExtraInputChange,
Fetched,
InputChange,
ParametersChange,
@@ -185,6 +186,7 @@ export type DBReducerActionType =
type:
| ActionType.ExtraEditorChange
| ActionType.ExtraInputChange
+ | ActionType.EncryptedExtraInputChange
| ActionType.TextChange
| ActionType.QueryChange
| ActionType.InputChange
@@ -269,6 +271,14 @@ export function dbReducer(
[action.payload.name]: actionPayloadJson,
}),
};
+ case ActionType.EncryptedExtraInputChange:
+ return {
+ ...trimmedState,
+ masked_encrypted_extra: JSON.stringify({
+ ...JSON.parse(trimmedState.masked_encrypted_extra || '{}'),
+ [action.payload.name]: action.payload.value,
+ }),
+ };
case ActionType.ExtraInputChange:
// "extra" payload in state is a string
if (
@@ -1656,6 +1666,16 @@ const DatabaseModal: FunctionComponent = ({
value: target.value,
})
}
+ onEncryptedExtraInputChange={({
+ target,
+ }: {
+ target: HTMLInputElement;
+ }) =>
+ onChange(ActionType.EncryptedExtraInputChange, {
+ name: target.name,
+ value: target.value,
+ })
+ }
onRemoveTableCatalog={(idx: number) => {
setDB({
type: ActionType.RemoveTableCatalogSheet,
diff --git a/superset-frontend/src/features/databases/types.ts b/superset-frontend/src/features/databases/types.ts
index c46296a2a40..c08cef1bcc5 100644
--- a/superset-frontend/src/features/databases/types.ts
+++ b/superset-frontend/src/features/databases/types.ts
@@ -113,6 +113,7 @@ export type DatabaseObject = {
supports_file_upload?: boolean;
disable_ssh_tunneling?: boolean;
supports_dynamic_catalog?: boolean;
+ supports_oauth2?: boolean;
};
// SSH Tunnel information
@@ -301,6 +302,7 @@ export interface FieldPropTypes {
onRemoveTableCatalog: (idx: number) => void;
} & {
onExtraInputChange: (value: any) => void;
+ onEncryptedExtraInputChange: (value: any) => void;
onSSHTunnelParametersChange: CustomEventHandlerType;
};
validationErrors: JsonObject | null;
diff --git a/superset/databases/api.py b/superset/databases/api.py
index 5cb91b95c05..c84af0f3807 100644
--- a/superset/databases/api.py
+++ b/superset/databases/api.py
@@ -1898,9 +1898,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
@protect()
@statsd_metrics
@event_logger.log_this_with_context(
- action=lambda self,
- *args,
- **kwargs: f"{self.__class__.__name__}.columnar_upload",
+ action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.columnar_upload",
log_to_statsd=False,
)
@requires_form_data
@@ -2073,7 +2071,6 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
"sqlalchemy_uri_placeholder": engine_spec.sqlalchemy_uri_placeholder,
"preferred": engine_spec.engine_name in preferred_databases,
"engine_information": engine_spec.get_public_information(),
- "supports_oauth2": engine_spec.supports_oauth2,
}
if engine_spec.default_driver:
diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py
index b7de28eadc6..8d40065c0ea 100644
--- a/superset/db_engine_specs/base.py
+++ b/superset/db_engine_specs/base.py
@@ -136,7 +136,9 @@ builtin_time_grains: dict[str | None, str] = {
}
-class TimestampExpression(ColumnClause): # pylint: disable=abstract-method, too-many-ancestors
+class TimestampExpression(
+ ColumnClause
+): # pylint: disable=abstract-method, too-many-ancestors
def __init__(self, expr: str, col: ColumnClause, **kwargs: Any) -> None:
"""Sqlalchemy class that can be used to render native column elements respecting
engine-specific quoting rules as part of a string-based expression.
@@ -394,9 +396,9 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
max_column_name_length: int | None = None
try_remove_schema_from_table_name = True # pylint: disable=invalid-name
run_multiple_statements_as_one = False
- custom_errors: dict[
- Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]
- ] = {}
+ custom_errors: dict[Pattern[str], tuple[str, SupersetErrorType, dict[str, Any]]] = (
+ {}
+ )
# Whether the engine supports file uploads
# if True, database will be listed as option in the upload file form
@@ -2196,6 +2198,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
"supports_file_upload": cls.supports_file_upload,
"disable_ssh_tunneling": cls.disable_ssh_tunneling,
"supports_dynamic_catalog": cls.supports_dynamic_catalog,
+ "supports_oauth2": cls.supports_oauth2,
}
@classmethod
diff --git a/superset/db_engine_specs/snowflake.py b/superset/db_engine_specs/snowflake.py
index dbe5310a2ba..b222f77a4b9 100644
--- a/superset/db_engine_specs/snowflake.py
+++ b/superset/db_engine_specs/snowflake.py
@@ -73,6 +73,11 @@ class SnowflakeParametersSchema(Schema):
allow_none=True,
metadata={"description": "Password"},
)
+ oauth2_client = fields.Str(
+ required=False,
+ allow_none=True,
+ metadata={"description": "OAuth2 client information"},
+ )
account = fields.Str(required=True, metadata={"description": "Account name"})
database = fields.Str(required=True, metadata={"description": "Database name"})
role = fields.Str(