From 1067d00eae2e02b0f44341bed55d865ea25f4208 Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Tue, 25 Feb 2025 18:43:42 +0100 Subject: [PATCH] CB-4067 adds SSL part --- .../ConnectionDriverPropertiesTabService.ts | 6 +- .../SSH/ConnectionSSHTabService.ts | 11 +- .../SSL/ConnectionFormSSLPart.ts | 208 ++++++++++++++++++ .../SSL/ConnectionSSLTabService.ts | 18 ++ .../SSL/IConnectionFromSSLState.ts | 12 + .../SSL/getConnectionFormSSLPart.ts | 25 +++ 6 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts create mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/SSL/IConnectionFromSSLState.ts create mode 100644 webapp/packages/plugin-connections/src/ConnectionForm/SSL/getConnectionFormSSLPart.ts diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts index 2b4dbc87b7..d8d9b26c4e 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabService.ts @@ -60,10 +60,8 @@ export class ConnectionDriverPropertiesTabService extends Bootstrap { // return true; // } - // const optionsPart = getConnectionFormOptionsPart(props.formState); - - // if (optionsPart.state.connectionConfig.driverId) { - // return !optionsPart.state.connectionConfig.driverId; + // if (props.formState.state.driverId) { + // return false; // } // return true; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts index 3ce608ad49..ac4f6db32f 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts @@ -74,13 +74,12 @@ export class ConnectionSSHTabService extends Bootstrap { // if (!props?.formState) { // return true; // } - // const optionsPart = getConnectionFormOptionsPart(props.formState); - // if (optionsPart.state.connectionConfig.driverId) { - // const driver = optionsPart.state.connectionConfig.driverId - // ? this.dbDriverResource.get(optionsPart.state.connectionConfig.driverId) - // : undefined; - // const urlType = optionsPart.state.connectionConfig.configurationType === DriverConfigurationType.Url; + // const driverId = props.formState.state.driverId; + + // if (driverId) { + // const driver = driverId ? this.dbDriverResource.get(driverId) : undefined; + // const urlType = optionsPart.state.configurationType === DriverConfigurationType.Url; // return urlType || !driver?.applicableNetworkHandlers.includes(SSH_TUNNEL_ID); // } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts new file mode 100644 index 0000000000..4e907bbda1 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionFormSSLPart.ts @@ -0,0 +1,208 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { FormPart, type IFormState } from '@cloudbeaver/core-ui'; + +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { IConnectionFormStateRefactored } from '../IConnectionFormStateRefactored.js'; +import type { NetworkHandlerConfigInput } from '@cloudbeaver/core-sdk'; +import { isNotNullDefined, isObjectsEqual } from '@cloudbeaver/core-utils'; +import { getSSLDriverHandler } from './getSSLDriverHandler.js'; +import { + createConnectionParam, + type ConnectionInfoResource, + type DBDriverResource, + type NetworkHandlerResource, +} from '@cloudbeaver/core-connections'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { getConnectionFormOptionsPart } from '../Options/getConnectionFormOptionsPart.js'; +import { getSSLDefaultConfig } from './getSSLDefaultConfig.js'; +import { toJS } from 'mobx'; +import { connectionCredentialsStateContext } from '../Contexts/connectionCredentialsStateContext.js'; +import { PROPERTY_FEATURE_SECURED } from './PROPERTY_FEATURE_SECURED.js'; +import { SSL_CODE_NAME } from './SSL_CODE_NAME.js'; + +export class ConnectionFormSSLPart extends FormPart { + constructor( + formState: IFormState, + private readonly dbDriverResource: DBDriverResource, + private readonly networkHandlerResource: NetworkHandlerResource, + private readonly connectionInfoResource: ConnectionInfoResource, + ) { + super(formState); + } + + protected override async loader(): Promise { + if (!this.isChanged || !this.formState.state.driverId) { + return; + } + + const driver = await this.dbDriverResource.load(this.formState.state.driverId); + const handlers = await this.networkHandlerResource.load(CachedMapAllKey); + + const optionsPart = getConnectionFormOptionsPart(this.formState); + const handler = getSSLDriverHandler(handlers, driver?.applicableNetworkHandlers ?? []); + const info = this.connectionInfoResource.get( + createConnectionParam({ + id: this.formState.state.connectionId, + projectId: this.formState.state.projectId, + }), + ); + + if (!handler) { + return; + } + + const initialConfig = info?.networkHandlersConfig?.find(h => h.id === handler.id); + + if (!optionsPart?.state.networkHandlersConfig) { + optionsPart.state.networkHandlersConfig = []; + } + + if (!optionsPart?.state.networkHandlersConfig.some(state => state.id === handler.id)) { + const config: NetworkHandlerConfigInput = initialConfig ? toJS(initialConfig) : getSSLDefaultConfig(handler.id); + + if (config.secureProperties) { + config.properties = { ...config.properties, ...config.secureProperties }; + } + + optionsPart?.state.networkHandlersConfig.push(config); + } + } + + protected override async format( + data: IFormState<{ projectId: string; connectionId: string; driverId: string; submitType: 'submit' | 'test' }>, + contexts: IExecutionContextProvider>, + ): Promise { + const optionsPart = getConnectionFormOptionsPart(this.formState); + const config = optionsPart.state; + const credentialsState = contexts.getContext(connectionCredentialsStateContext); + const info = this.connectionInfoResource.get( + createConnectionParam({ + id: data.state.connectionId, + projectId: data.state.projectId, + }), + ); + + if (!config.networkHandlersConfig || config.networkHandlersConfig.length === 0 || !config.driverId) { + return; + } + + const driver = await this.dbDriverResource.load(config.driverId); + const handlers = await this.networkHandlerResource.load(CachedMapAllKey); + const handler = config.networkHandlersConfig.find( + handler => driver?.applicableNetworkHandlers.includes(handler.id) && handlers.some(h => h.id === handler.id && h.codeName === SSL_CODE_NAME), + ); + const descriptor = handlers.find(h => h.id === handler?.id); + + if (!handler) { + return; + } + + const initial = info?.networkHandlersConfig?.find(h => h.id === handler.id); + const handlerConfig: NetworkHandlerConfigInput = toJS(handler); + handlerConfig.savePassword = handler.savePassword || config.sharedCredentials; + + const changed = isChanged(handlerConfig, initial); + + if (changed && descriptor) { + for (const descriptorProperty of descriptor.properties) { + if (!descriptorProperty.id) { + continue; + } + + const key = descriptorProperty.id; + const isDefault = isNotNullDefined(descriptorProperty.defaultValue); + + if (!(key in handlerConfig.properties) && isDefault) { + handlerConfig.properties[key] = descriptorProperty.defaultValue; + } + + const secured = descriptorProperty.features.includes(PROPERTY_FEATURE_SECURED); + + if (secured) { + const value = handlerConfig.properties[key]; + const propertyChanged = initial?.secureProperties?.[key] !== value; + + if (propertyChanged) { + handlerConfig.secureProperties[key] = toJS(value); + } else { + delete handlerConfig.secureProperties[key]; + } + + delete handlerConfig.properties[key]; + } + } + + if (this.formState.state.submitType === 'submit') { + if (Object.keys(handlerConfig.secureProperties).length === 0) { + delete handlerConfig.secureProperties; + } + + if (Object.keys(handlerConfig.properties).length === 0) { + delete handlerConfig.properties; + } + } + } + + if (handler.enabled && !handler.savePassword) { + credentialsState.requireNetworkHandler(handler.id); + } + + if (changed) { + if (!config.networkHandlersConfig) { + config.networkHandlersConfig = []; + } + + trimSSLConfig(handlerConfig); + config.networkHandlersConfig.push(handlerConfig); + } + } + + protected override saveChanges( + data: IFormState, + contexts: IExecutionContextProvider>, + ): Promise { + return Promise.resolve(); + } +} + +function trimSSLConfig(input: NetworkHandlerConfigInput) { + const { secureProperties } = input; + + if (!secureProperties) { + return; + } + + if (!Object.keys(secureProperties).length) { + return; + } + + for (const key in secureProperties) { + if (typeof secureProperties[key] === 'string') { + secureProperties[key] = secureProperties[key]?.trim(); + } + } +} + +function isChanged(handler: NetworkHandlerConfigInput, initial?: NetworkHandlerConfigInput) { + if (!initial && !handler.enabled) { + return false; + } + + const initialProperties = { ...(initial?.properties ?? {}), ...(initial?.secureProperties ?? {}) }; + + if ( + handler.enabled !== initial?.enabled || + handler.savePassword !== initial?.savePassword || + !isObjectsEqual(handler.properties, initialProperties) + ) { + return true; + } + + return false; +} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts index 623f739cd1..59e461d3b7 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts @@ -41,6 +41,7 @@ export class ConnectionSSLTabService extends Bootstrap { private readonly connectionFormService: ConnectionFormService, private readonly dbDriverResource: DBDriverResource, private readonly networkHandlerResource: NetworkHandlerResource, + // private readonly connectionFormServiceRefactored: ConnectionFormServiceRefactored, ) { super(); @@ -67,6 +68,23 @@ export class ConnectionSSLTabService extends Bootstrap { }, }); + // TODO uncomment once refactored + // this.connectionFormServiceRefactored.parts.add({ + // key: 'ssl', + // order: 4, + // tab: () => SSLTab, + // panel: () => SSLPanel, + // isHidden: (_, props) => { + // if (props?.formState.state.driverId) { + // const driver = this.dbDriverResource.get(props.formState.state.driverId); + // const handler = getSSLDriverHandler(this.networkHandlerResource.values, driver?.applicableNetworkHandlers ?? []); + // return !handler; + // } + + // return true; + // }, + // }); + this.connectionFormService.prepareConfigTask.addHandler(this.prepareConfig.bind(this)); this.connectionFormService.formStateTask.addHandler(this.formState.bind(this)); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/IConnectionFromSSLState.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/IConnectionFromSSLState.ts new file mode 100644 index 0000000000..07ed0a5ae7 --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/IConnectionFromSSLState.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +export const CONNECTION_FORM_SSL_SCHEMA = schema.object({}); + +export type IConnectionFromSSLState = schema.infer; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getConnectionFormSSLPart.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getConnectionFormSSLPart.ts new file mode 100644 index 0000000000..890de8a0da --- /dev/null +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/getConnectionFormSSLPart.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import type { IFormState } from '@cloudbeaver/core-ui'; +import { ConnectionFormSSLPart } from './ConnectionFormSSLPart.js'; +import type { IConnectionFormStateRefactored } from '../IConnectionFormStateRefactored.js'; +import { ConnectionInfoResource, DBDriverResource, NetworkHandlerResource } from '@cloudbeaver/core-connections'; + +const DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART = createDataContext('Connection Form SSL Part'); + +export function getConnectionFormSSLPart(formState: IFormState): ConnectionFormSSLPart { + return formState.getPart(DATA_CONTEXT_CONNECTION_FORM_OPTIONS_PART, context => { + const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; + const dbDriverResource = di.getService(DBDriverResource); + const networkHandlerResource = di.getService(NetworkHandlerResource); + const connectionInfoResource = di.getService(ConnectionInfoResource); + + return new ConnectionFormSSLPart(formState, dbDriverResource, networkHandlerResource, connectionInfoResource); + }); +}