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(