diff --git a/README.md b/README.md index fecb8e19a2..966c2a2aab 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,11 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### 23.3.5. 2024-02-19 +- Default user group was added to the product. This group includes all users. You cannot delete users from this group; +- Added the ability to disable alias autocomplete for all users in the admin panel; +- Different bug fixes and enhancements have been made. + ### 23.3.4. 2024-02-05 - Text wrap is activated by default for texts and BLOBs in the Values panel for better visibility. User can switch to the one-line mode using a button on the toolbar; - Added the ability to edit the default preferences of the following parts: interface, tools and data viewer in the settings panel in the administrative part; @@ -42,10 +47,3 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io - Added the ability to view decoded binary-type data in the Value panel; - Enhanced security for unauthorized access; - Different bug fixes and enhancements have been made. - - - -### Old CloudBeaver releases - -You can find information about earlier releases on the CloudBeaver wiki https://github.com/dbeaver/cloudbeaver/wiki/Releases. - diff --git a/deploy/build-backend.sh b/deploy/build-backend.sh index 0704147375..4593f944bb 100755 --- a/deploy/build-backend.sh +++ b/deploy/build-backend.sh @@ -23,7 +23,9 @@ echo "Pull cloudbeaver platform" cd ../.. echo "Pull dbeaver platform" -[ ! -d dbeaver ] && git clone https://github.com/dbeaver/dbeaver.git +[ ! -d dbeaver ] && git clone --depth 1 https://github.com/dbeaver/dbeaver.git +[ ! -d dbeaver-common ] && git clone --depth 1 https://github.com/dbeaver/dbeaver-common.git + cd cloudbeaver/deploy diff --git a/deploy/build.bat b/deploy/build.bat index 28204e7a72..e5a90eb1df 100644 --- a/deploy/build.bat +++ b/deploy/build.bat @@ -24,6 +24,7 @@ cd ..\.. echo Pull dbeaver platform IF NOT EXIST dbeaver git clone https://github.com/dbeaver/dbeaver.git +IF NOT EXIST dbeaver-common git clone https://github.com/dbeaver/dbeaver-common.git cd cloudbeaver\deploy echo Build cloudbeaver server diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index b57e8ed021..b4a22bb6a0 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -448,5 +448,12 @@ public int getKeepAliveInterval() { return dataSourceContainer.getConnectionConfiguration().getKeepAliveInterval(); } + @Property + public List getSharedSecrets() throws DBException { + return dataSourceContainer.listSharedCredentials() + .stream() + .map(WebSecretInfo::new) + .collect(Collectors.toList()); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java new file mode 100644 index 0000000000..627947a358 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebSecretInfo.java @@ -0,0 +1,38 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed 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. + */ +package io.cloudbeaver.model; + +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; + +public class WebSecretInfo { + private final DBSSecretValue secretValue; + + public WebSecretInfo(DBSSecretValue secretValue) { + this.secretValue = secretValue; + } + + @Property + public String getDisplayName() { + return secretValue.getDisplayName(); + } + + @Property + public String getSecretId() { + return secretValue.getUniqueId(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java index 5c178215d9..2d8e5f07bb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebAuthInfo.java @@ -22,6 +22,7 @@ import io.cloudbeaver.registry.WebAuthProviderDescriptor; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.auth.SMAuthInfo; import org.jkiss.dbeaver.model.auth.SMAuthProvider; import org.jkiss.dbeaver.model.auth.SMSession; import org.jkiss.dbeaver.model.auth.SMSessionPrincipal; @@ -41,7 +42,10 @@ public class WebAuthInfo implements SMSessionPrincipal { private final WebUser user; private final WebAuthProviderDescriptor authProvider; private String authProviderConfigurationId; - private SMSession authSession; + @NotNull + private final SMAuthInfo authInfo; + @NotNull + private final SMSession authSession; private final OffsetDateTime loginTime; private final DBWUserIdentity userIdentity; private String message; @@ -54,12 +58,14 @@ public WebAuthInfo( @NotNull WebAuthProviderDescriptor authProvider, @NotNull DBWUserIdentity userIdentity, @NotNull SMSession authSession, + @NotNull SMAuthInfo authInfo, @NotNull OffsetDateTime loginTime ) { this.session = session; this.user = user; this.authProvider = authProvider; this.userIdentity = userIdentity; + this.authInfo = authInfo; this.authSession = authSession; this.loginTime = loginTime; } @@ -120,10 +126,16 @@ public WebAuthProviderDescriptor getAuthProviderDescriptor() { return authProvider; } + @NotNull public SMSession getAuthSession() { return authSession; } + @NotNull + public SMAuthInfo getAuthInfo() { + return authInfo; + } + void closeAuth() { if (authProvider != null && authSession != null) { try { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index d975fbe207..73f1f0b61b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -91,7 +91,7 @@ * Is the main source of data in web application */ public class WebSession extends BaseWebSession - implements SMSession, SMCredentialsProvider, DBACredentialsProvider, IAdaptable { + implements SMSessionWithAuth, SMCredentialsProvider, DBACredentialsProvider, IAdaptable { private static final Log log = Log.getLog(WebSession.class); @@ -813,6 +813,14 @@ public WebAuthInfo getAuthInfo(@Nullable String providerID) { } } + @Override + public List getAuthInfos() { + synchronized (authTokens) { + return authTokens.stream().map(WebAuthInfo::getAuthInfo).toList(); + } + } + + public List getAllAuthInfo() { synchronized (authTokens) { return new ArrayList<>(authTokens); @@ -1008,6 +1016,17 @@ public WebProjectImpl getProjectById(@Nullable String projectId) { return getWorkspace().getProjectById(projectId); } + public WebProjectImpl getAccessibleProjectById(@Nullable String projectId) throws DBWebException { + WebProjectImpl project = null; + if (projectId != null) { + project = getWorkspace().getProjectById(projectId); + } + if (project == null) { + throw new DBWebException("Project not found: " + projectId); + } + return project; + } + public List getAccessibleProjects() { return getWorkspace().getProjects(); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java index 82b5542b50..f695ab401a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSessionAuthProcessor.java @@ -164,6 +164,7 @@ private List finishWebSessionAuthorization(SMAuthInfo authInfo) thr authProviderDescriptor, userIdentity, authSession, + authInfo, OffsetDateTime.now() ); webAuthInfo.setAuthProviderConfigurationId(authConfiguration.getAuthProviderConfigurationId()); diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 0b2124d5e9..e0b7e6ae7b 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -308,6 +308,11 @@ type NetworkHandlerConfig { secureProperties: Object! } +type SecretInfo { + displayName: String! + secretId: String! +} + # Connection instance type ConnectionInfo { id: ID! @@ -337,6 +342,8 @@ type ConnectionInfo { saveCredentials: Boolean! # Shared credentials - the same for all users, stored in secure storage. sharedCredentials: Boolean! + + sharedSecrets: [SecretInfo!]! @since(version: "23.3.5") # Determines that credentials were saved for current user. # This field read is slow, it should be read only when it really needed credentialsSaved: Boolean! @@ -488,6 +495,7 @@ input ConnectionConfig { saveCredentials: Boolean sharedCredentials: Boolean authModelId: ID + selectedSecretId: ID @since(version: "23.3.5") credentials: Object # Map of provider properties (name/value) @@ -584,14 +592,14 @@ extend type Mutation { copyConnectionFromNode( nodePath: String!, config: ConnectionConfig, projectId: ID ): ConnectionInfo! # Test connection configuration. Returns remote server version - testConnection( config: ConnectionConfig!, projectId: ID ): ConnectionInfo! + testConnection( config: ConnectionConfig!, projectId: ID): ConnectionInfo! # Test connection configuration. Returns remote server version testNetworkHandler( config: NetworkHandlerConfigInput! ): NetworkEndpointInfo! # Initiate existing connection initConnection( id: ID!, projectId: ID, credentials: Object, networkCredentials: [NetworkHandlerConfigInput!], - saveCredentials:Boolean, sharedCredentials: Boolean ): ConnectionInfo! + saveCredentials:Boolean, sharedCredentials: Boolean, selectedSecretId:String ): ConnectionInfo! # Disconnect from database closeConnection( id: ID!, projectId: ID ): ConnectionInfo! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java index ade7f8c719..758ece7742 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionConfig.java @@ -16,6 +16,7 @@ */ package io.cloudbeaver.model; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.meta.Property; @@ -59,6 +60,7 @@ public class WebConnectionConfig { private Map providerProperties; private List networkHandlersConfig; private DBPDriverConfigurationType configurationType; + private String selectedSecretId; public WebConnectionConfig() { } @@ -91,6 +93,7 @@ public WebConnectionConfig(Map params) { properties = JSONUtils.getObjectOrNull(params, "properties"); userName = JSONUtils.getString(params, "userName"); userPassword = JSONUtils.getString(params, "userPassword"); + selectedSecretId = JSONUtils.getString(params, "selectedSecretId"); authModelId = JSONUtils.getString(params, "authModelId"); credentials = JSONUtils.getObjectOrNull(params, "credentials"); @@ -231,4 +234,9 @@ public Map getProviderProperties() { public Integer getKeepAliveInterval() { return keepAliveInterval; } + + @Nullable + public String getSelectedSecretId() { + return selectedSecretId; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index ab620fc175..65007f34b9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -113,7 +113,8 @@ WebConnectionInfo initConnection( @NotNull Map authProperties, @Nullable List networkCredentials, @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials + @Nullable Boolean sharedCredentials, + @Nullable String selectedCredentials ) throws DBWebException; @WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT}) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 80a416f261..76f2e718cd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -132,7 +132,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("credentials"), nhc, env.getArgument("saveCredentials"), - env.getArgument("sharedCredentials") + env.getArgument("sharedCredentials"), + env.getArgument("selectedSecretId") ); } ) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 2e60d03d4a..5707ac37ca 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -54,6 +54,7 @@ import org.jkiss.dbeaver.model.rm.RMProjectType; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.secret.DBSSecretController; +import org.jkiss.dbeaver.model.secret.DBSSecretValue; import org.jkiss.dbeaver.model.websocket.WSConstants; import org.jkiss.dbeaver.model.websocket.event.datasource.WSDataSourceProperty; import org.jkiss.dbeaver.registry.DataSourceDescriptor; @@ -318,15 +319,32 @@ public WebConnectionInfo initConnection( @NotNull Map authProperties, @Nullable List networkCredentials, @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials + @Nullable Boolean sharedCredentials, + @Nullable String selectedSecretId ) throws DBWebException { WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, connectionId); connectionInfo.setSavedCredentials(authProperties, networkCredentials); - DBPDataSourceContainer dataSourceContainer = connectionInfo.getDataSourceContainer(); + var dataSourceContainer = (DataSourceDescriptor) connectionInfo.getDataSourceContainer(); if (dataSourceContainer.isConnected()) { throw new DBWebException("Datasource '" + dataSourceContainer.getName() + "' is already connected"); } + if (dataSourceContainer.isSharedCredentials() && selectedSecretId != null) { + List allSecrets; + try { + allSecrets = dataSourceContainer.listSharedCredentials(); + } catch (DBException e) { + throw new DBWebException("Error loading connection secret", e); + } + DBSSecretValue selectedSecret = + allSecrets.stream() + .filter(secret -> selectedSecretId.equals(secret.getUniqueId())) + .findFirst().orElse(null); + if (selectedSecret == null) { + throw new DBWebException("Secret not found:" + selectedSecretId); + } + dataSourceContainer.setSelectedSharedCredentials(selectedSecret); + } boolean oldSavePassword = dataSourceContainer.isSavePassword(); try { @@ -642,12 +660,12 @@ public WebConnectionInfo testConnection( connectionConfig.setSaveCredentials(true); // It is used in createConnectionFromConfig - DBPDataSourceContainer dataSource = WebDataSourceUtils.getLocalOrGlobalDataSource( + DataSourceDescriptor dataSource = (DataSourceDescriptor) WebDataSourceUtils.getLocalOrGlobalDataSource( CBApplication.getInstance(), webSession, projectId, connectionId); WebProjectImpl project = getProjectById(webSession, projectId); DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); - DBPDataSourceContainer testDataSource; + DataSourceDescriptor testDataSource; if (dataSource != null) { try { // Check that creds are saved to trigger secrets resolve @@ -656,12 +674,27 @@ public WebConnectionInfo testConnection( throw new DBWebException("Can't determine whether datasource credentials are saved", e); } - testDataSource = dataSource.createCopy(dataSource.getRegistry()); + testDataSource = (DataSourceDescriptor) dataSource.createCopy(dataSource.getRegistry()); WebServiceUtils.setConnectionConfiguration( testDataSource.getDriver(), testDataSource.getConnectionConfiguration(), connectionConfig ); + if (connectionConfig.getSelectedSecretId() != null) { + try { + DBSSecretValue secretValue = dataSource.listSharedCredentials() + .stream() + .filter(secret -> connectionConfig.getSelectedSecretId().equals(secret.getSubjectId())) + .findFirst() + .orElse(null); + + if (secretValue != null) { + testDataSource.setSelectedSharedCredentials(secretValue); + } + } catch (DBException e) { + throw new DBWebException("Failed to load secret value: " + connectionConfig.getSelectedSecretId()); + } + } WebServiceUtils.saveAuthProperties( testDataSource, testDataSource.getConnectionConfiguration(), @@ -671,7 +704,8 @@ public WebConnectionInfo testConnection( true ); } else { - testDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry); + testDataSource = (DataSourceDescriptor) WebServiceUtils.createConnectionFromConfig(connectionConfig, + sessionRegistry); } webSession.provideAuthParameters(webSession.getProgressMonitor(), testDataSource, testDataSource.getConnectionConfiguration()); testDataSource.setSavePassword(true); // We need for test to avoid password callback diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPConstants.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPConstants.java index 304be1493a..ec933d3ca2 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPConstants.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPConstants.java @@ -23,4 +23,5 @@ interface RPConstants { String PARAM_FIRST_NAME = "first-name-header"; String PARAM_LAST_NAME = "last-name-header"; String PARAM_ROLE_NAME = "role-header"; + String PARAM_TEAM_DELIMITER = "team-delimiter"; } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java index 4088b0beaa..020d370df0 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java @@ -33,6 +33,7 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.auth.SMAuthInfo; +import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; import org.jkiss.dbeaver.model.security.SMConstants; import org.jkiss.dbeaver.model.security.SMController; @@ -50,6 +51,7 @@ public class RPSessionHandler implements DBWSessionHandler { private static final Log log = Log.getLog(RPSessionHandler.class); + public static final String DEFAULT_TEAM_DELIMITER = "\\|"; @Override public boolean handleSessionOpen(WebSession webSession, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException { @@ -87,15 +89,18 @@ public void reverseProxyAuthentication(@NotNull HttpServletRequest request, @Not resolveParam(paramConfigMap.get(RPConstants.PARAM_USER), RPAuthProvider.X_USER) ); String teams = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_TEAM), RPAuthProvider.X_TEAM)); - if (CommonUtils.isEmpty(teams)) { - // backward compatibility - teams = request.getHeader(RPAuthProvider.X_ROLE); + // backward compatibility + String deprecatedTeams = request.getHeader(RPAuthProvider.X_ROLE); + if (teams == null && deprecatedTeams != null) { + teams = deprecatedTeams; } String role = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_ROLE_NAME), RPAuthProvider.X_ROLE_TE)); String firstName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_FIRST_NAME), RPAuthProvider.X_FIRST_NAME)); String lastName = request.getHeader(resolveParam(paramConfigMap.get(RPConstants.PARAM_LAST_NAME), RPAuthProvider.X_LAST_NAME)); String logoutUrl = Objects.requireNonNull(configuration).getParameter(RPConstants.PARAM_LOGOUT_URL); - List userTeams = teams == null ? Collections.emptyList() : List.of(teams.split("\\|")); + String teamDelimiter = JSONUtils.getString(configuration.getParameters(), + RPConstants.PARAM_TEAM_DELIMITER, "\\|"); + List userTeams = teams == null ? null : (teams.isEmpty() ? List.of() : List.of(teams.split(teamDelimiter))); if (userName != null) { try { Map credentials = new HashMap<>(); diff --git a/server/bundles/io.cloudbeaver.service.security/plugin.xml b/server/bundles/io.cloudbeaver.service.security/plugin.xml index 5878dca3c7..f3389a96fc 100644 --- a/server/bundles/io.cloudbeaver.service.security/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.security/plugin.xml @@ -31,9 +31,10 @@ + - + diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts index 144ce9e331..bf2066c28a 100644 --- a/webapp/packages/core-authentication/src/AppAuthService.ts +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -23,8 +23,8 @@ export class AppAuthService extends Bootstrap { get loaders(): ILoadableState[] { return [ - getCachedDataResourceLoaderState(this.userInfoResource, undefined), - getCachedDataResourceLoaderState(this.serverConfigResource, undefined), + getCachedDataResourceLoaderState(this.userInfoResource, () => undefined), + getCachedDataResourceLoaderState(this.serverConfigResource, () => undefined), ]; } diff --git a/webapp/packages/core-blocks/public/icons/success_sm.svg b/webapp/packages/core-blocks/public/icons/success_sm.svg index 818bebf74f..ed6811d778 100644 --- a/webapp/packages/core-blocks/public/icons/success_sm.svg +++ b/webapp/packages/core-blocks/public/icons/success_sm.svg @@ -1,3 +1,3 @@ - + diff --git a/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx b/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx index ef394cb054..0004d41f46 100644 --- a/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx +++ b/webapp/packages/core-blocks/src/CommonDialog/RenameDialog.tsx @@ -16,7 +16,7 @@ import { Button } from '../Button'; import { Container } from '../Containers/Container'; import { Fill } from '../Fill'; import { Form } from '../FormControls/Form'; -import { InputField } from '../FormControls/InputField'; +import { InputField } from '../FormControls/InputField/InputField'; import { useTranslate } from '../localization/useTranslate'; import { s } from '../s'; import { useFocus } from '../useFocus'; diff --git a/webapp/packages/core-blocks/src/Containers/Group.m.css b/webapp/packages/core-blocks/src/Containers/Group.m.css index dae0c832fc..c2f128d261 100644 --- a/webapp/packages/core-blocks/src/Containers/Group.m.css +++ b/webapp/packages/core-blocks/src/Containers/Group.m.css @@ -1,6 +1,18 @@ -.group { +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.secondary { + composes: theme-background-secondary theme-text-on-secondary from global; +} +.surface { composes: theme-background-surface theme-text-on-surface from global; - +} +.group { align-content: baseline; box-sizing: border-box; padding: 24px; diff --git a/webapp/packages/core-blocks/src/Containers/Group.tsx b/webapp/packages/core-blocks/src/Containers/Group.tsx index 3bcdca638b..1f33020ae2 100644 --- a/webapp/packages/core-blocks/src/Containers/Group.tsx +++ b/webapp/packages/core-blocks/src/Containers/Group.tsx @@ -17,13 +17,14 @@ import elementsSizeStyles from './shared/ElementsSize.m.css'; interface Props extends IContainerProps { form?: boolean; + secondary?: boolean; center?: boolean; box?: boolean; boxNoOverflow?: boolean; } export const Group = forwardRef>(function Group( - { form, center, box, boxNoOverflow, className, ...rest }, + { form, center, box, secondary, boxNoOverflow, className, ...rest }, ref, ) { const styles = useS(style, containerStyles, elementsSizeStyles); @@ -40,6 +41,8 @@ export const Group = forwardRef .fill { + flex-grow: 1; max-width: none; } diff --git a/webapp/packages/core-blocks/src/ExceptionMessage.tsx b/webapp/packages/core-blocks/src/ExceptionMessage.tsx index 77de07eb02..0a4ebdb3f8 100644 --- a/webapp/packages/core-blocks/src/ExceptionMessage.tsx +++ b/webapp/packages/core-blocks/src/ExceptionMessage.tsx @@ -17,7 +17,7 @@ import { useErrorDetails } from './useErrorDetails'; import { useS } from './useS'; interface Props { - exception?: Error; + exception?: Error | null; icon?: boolean; inline?: boolean; className?: string; @@ -39,6 +39,10 @@ export const ExceptionMessage = observer(function ExceptionMessage({ exce }; } + if (!exception) { + return null; + } + return (
@@ -51,7 +55,8 @@ export const ExceptionMessage = observer(function ExceptionMessage({ exce {translate('core_blocks_exception_message_error_title')}
- {translate('core_blocks_exception_message_error_message')} {onRetry && translate('ui_please_retry')} + {(error.hasDetails && error.message) || translate('core_blocks_exception_message_error_message')}{' '} + {onRetry && translate('ui_please_retry')}
+ {!disableTest && ( + + )} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index e886f75a31..86c45f8abd 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -316,8 +316,8 @@ export class ConnectionOptionsTabService extends Bootstrap { if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) { tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel; - tempConfig.saveCredentials = state.config.saveCredentials; tempConfig.sharedCredentials = state.config.sharedCredentials; + tempConfig.saveCredentials = state.config.saveCredentials || tempConfig.sharedCredentials; const properties = await this.getConnectionAuthModelProperties(tempConfig.authModelId, state.info); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index ec25a5f123..12fa37d88d 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useRef } from 'react'; +import { useContext, useRef } from 'react'; import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { @@ -20,29 +20,29 @@ import { Group, GroupTitle, InputField, - ObjectPropertyInfoForm, + Link, Radio, RadioGroup, s, Textarea, useAdministrationSettings, useFormValidator, - usePermission, useResource, useS, useTranslate, } from '@cloudbeaver/core-blocks'; import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; -import { CachedResourceListEmptyKey, resourceKeyList } from '@cloudbeaver/core-resource'; -import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import { DriverConfigurationType } from '@cloudbeaver/core-sdk'; -import { type TabContainerPanelComponent, useAuthenticationAction } from '@cloudbeaver/core-ui'; -import { isSafari } from '@cloudbeaver/core-utils'; +import { type TabContainerPanelComponent, TabsContext, useAuthenticationAction } from '@cloudbeaver/core-ui'; import { ProjectSelect } from '@cloudbeaver/plugin-projects'; +import { ConnectionAuthModelCredentialsForm } from '../ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm'; +import { ConnectionAuthModelSelector } from '../ConnectionAuthModelCredentials/ConnectionAuthModelSelector'; import { ConnectionFormService } from '../ConnectionFormService'; import type { IConnectionFormProps } from '../IConnectionFormProps'; +import { CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID } from '../SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID'; import { ConnectionOptionsTabService } from './ConnectionOptionsTabService'; import styles from './Options.m.css'; import { ParametersForm } from './ParametersForm'; @@ -77,26 +77,15 @@ export const Options: TabContainerPanelComponent = observe const translate = useTranslate(); const { info, config, availableDrivers, submittingTask: submittingHandlers, disabled } = state; const style = useS(styles); + const tabsState = useContext(TabsContext); //@TODO it's here until the profile implementation in the CloudBeaver const readonly = state.readonly || info?.authModel === PROFILE_AUTH_MODEL_ID; - const adminPermission = usePermission(EAdminPermission.admin); - useFormValidator(submittingHandlers.for(service.formValidationTask), formRef.current); const optionsHook = useOptions(state); const { credentialsSavingEnabled } = useAdministrationSettings(); - const handleAuthModelSelect = useCallback(async (value?: string, name?: string, prev?: string) => { - const model = applicableAuthModels.find(model => model?.id === value); - - if (!model) { - return; - } - - optionsHook.setAuthModel(model); - }, []); - const driverMap = useResource( Options, DBDriverResource, @@ -119,25 +108,11 @@ export const Options: TabContainerPanelComponent = observe if (config.template) { config.folder = undefined; } - - if (name === 'sharedCredentials' && value) { - config.saveCredentials = true; - - for (const handler of config.networkHandlersConfig ?? []) { - if (!handler.savePassword) { - handler.savePassword = true; - } - } - } } - const { data: applicableAuthModels } = useResource( - Options, - DatabaseAuthModelsResource, - driver?.applicableAuthModels ? resourceKeyList(driver.applicableAuthModels) : CachedResourceListEmptyKey, - ); + const applicableAuthModels = driver?.applicableAuthModels ?? []; - const { data: authModel } = useResource( + const authModelLoader = useResource( Options, DatabaseAuthModelsResource, getComputed(() => config.authModelId || info?.authModel || driver?.defaultAuthModel || null), @@ -146,6 +121,22 @@ export const Options: TabContainerPanelComponent = observe }, ); + const authModel = authModelLoader.data; + + async function handleAuthModelSelect(id: string | undefined) { + if (!id) { + return; + } + + const model = await authModelLoader.resource.load(id); + + if (!model) { + return; + } + + optionsHook.setAuthModel(model); + } + const authentication = useAuthenticationAction({ providerId: authModel?.requiredAuth ?? info?.requiredAuth ?? AUTH_PROVIDER_LOCAL_ID, }); @@ -154,7 +145,6 @@ export const Options: TabContainerPanelComponent = observe const edit = state.mode === 'edit'; const originLocal = !info || isLocalConnection(info); - const availableAuthModels = applicableAuthModels.filter(model => !!model && (adminPermission || !model.requiresLocalConfiguration)); const drivers = driverMap.resource.enabledDrivers.filter(({ id }) => availableDrivers.includes(id)); let properties = authModel?.properties; @@ -163,6 +153,13 @@ export const Options: TabContainerPanelComponent = observe properties = info.authProperties; } + const sharedCredentials = config.sharedCredentials && serverConfigResource.data?.distributed; + + function openCredentialsTab(event: React.MouseEvent) { + event.preventDefault(); + tabsState?.open(CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID); + } + return (
@@ -285,63 +282,56 @@ export const Options: TabContainerPanelComponent = observe {!driver?.anonymousAccess && (authentication.authorized || !edit) && ( {translate('connections_connection_edit_authentication')} - {availableAuthModels.length > 1 && ( - model!.id} - valueSelector={model => model!.displayName} - titleSelector={model => model?.description} - searchable={availableAuthModels.length > 10} - readOnly={readonly || !originLocal} - disabled={disabled} - tiny - fill - onSelect={handleAuthModelSelect} - /> + disabled={disabled || readonly} + keepSize + > + {translate('connections_connection_share_credentials')} + )} - {authModel && properties && ( + + {!sharedCredentials ? ( <> - - - - {credentialsSavingEnabled && !config.template && ( - - - {translate('connections_connection_edit_save_credentials')} - - {serverConfigResource.resource.distributed && connectionOptionsTabService.isProjectShared(state) && ( - - {translate('connections_connection_share_credentials')} - - )} - )} + ) : ( + + {translate('plugin_connections_connection_form_shared_credentials_manage_info')} + + {translate('plugin_connections_connection_form_shared_credentials_manage_info_tab_link')} + + + )} + {!sharedCredentials && authModel && credentialsSavingEnabled && !config.template && ( + + {translate('connections_connection_edit_save_credentials')} + )} )} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts index 927eb5ad68..6a0c8dbef4 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/ConnectionSSHTabService.ts @@ -176,6 +176,7 @@ export class ConnectionSSHTabService extends Bootstrap { if (this.isChanged(handler, initial) || passwordChanged || keyChanged) { handlerConfig = { ...handler, + savePassword: handler.savePassword || config.sharedCredentials, key: handler.authType === NetworkHandlerAuthType.PublicKey && keyChanged ? handler.key : undefined, password: passwordChanged ? handler.password : undefined, }; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx index 4e8ffb20af..8c594677d3 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSH/SSH.tsx @@ -147,13 +147,8 @@ export const SSH: TabContainerPanelComponent = observer(function SSH({ st {keyAuth && } - {credentialsSavingEnabled && !formState.config.template && ( - + {credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && ( + {translate('connections_connection_edit_save_credentials')} )} diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts index dd3df9c23e..34c58c1551 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/ConnectionSSLTabService.ts @@ -136,6 +136,7 @@ export class ConnectionSSLTabService extends Bootstrap { const initial = state.info?.networkHandlersConfig?.find(h => h.id === handler.id); const handlerConfig: NetworkHandlerConfigInput = toJS(handler); + handlerConfig.savePassword = handler.savePassword || config.sharedCredentials; const changed = this.isChanged(handlerConfig, initial); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx index cc86b30ebc..68f75ea1fc 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/SSL/SSL.tsx @@ -102,7 +102,7 @@ export const SSL: TabContainerPanelComponent = observer(function SSL({ st ))} - {credentialsSavingEnabled && !formState.config.template && ( + {credentialsSavingEnabled && !formState.config.template && !formState.config.sharedCredentials && ( = observer(function DatabaseAuthDialog({ payload, options, rejectDialog, resolveDialog }) { - const styles = useS(style); + const connectionInfoLoader = useResource(DatabaseAuthDialog, ConnectionInfoResource, { + key: payload.connection, + includes: ['includeAuthNeeded', 'includeSharedSecrets', 'includeNetworkHandlersConfig', 'includeCredentialsSaved'], + }); const translate = useTranslate(); - const [focusedRef] = useFocus({ focusFirstChild: true }); + const driverLoader = useResource(DatabaseAuthDialog, DBDriverResource, connectionInfoLoader.data?.driverId || null); + const useSharedCredentials = (connectionInfoLoader.data?.sharedSecrets?.length || 0) > 1; - const { credentialsSavingEnabled } = useAdministrationSettings(); - const dialog = useDatabaseAuthDialog(payload.connection, payload.networkHandlers, payload.resetCredentials, resolveDialog); - const errorDetails = useErrorDetails(dialog.authException); + let subtitle = connectionInfoLoader.data?.name; - useAutoLoad(DatabaseAuthDialog, dialog); + if (useSharedCredentials) { + subtitle = [subtitle, translate('plugin_connections_connection_auth_secret_description')].join(' | '); + } return ( - + - - - - - - - - - - {dialog.authException && ( - - )} - - + {useSharedCredentials ? ( + + ) : ( + + )} ); }); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.m.css b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.m.css similarity index 100% rename from webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.m.css rename to webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.m.css diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx new file mode 100644 index 0000000000..2aebbf2f87 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialog.tsx @@ -0,0 +1,87 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { observer } from 'mobx-react-lite'; + +import { + CommonDialogBody, + CommonDialogFooter, + ErrorMessage, + Form, + Loader, + s, + useAdministrationSettings, + useAutoLoad, + useErrorDetails, + useFocus, + useS, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import type { IConnectionInfoParams } from '@cloudbeaver/core-connections'; + +import { ConnectionAuthenticationFormLoader } from '../../ConnectionAuthentication/ConnectionAuthenticationFormLoader'; +import style from './DatabaseCredentialsAuthDialog.m.css'; +import { DatabaseCredentialsAuthDialogFooter } from './DatabaseCredentialsAuthDialogFooter'; +import { useDatabaseCredentialsAuthDialog } from './useDatabaseCredentialsAuthDialog'; + +interface Props { + connection: IConnectionInfoParams; + networkHandlers: string[]; + resetCredentials?: boolean; + onLogin?: () => void; +} + +export const DatabaseCredentialsAuthDialog = observer(function DatabaseCredentialsAuthDialog({ + connection, + networkHandlers, + resetCredentials, + onLogin, +}) { + const styles = useS(style); + const translate = useTranslate(); + const [focusedRef] = useFocus({ focusFirstChild: true }); + + const { credentialsSavingEnabled } = useAdministrationSettings(); + const dialog = useDatabaseCredentialsAuthDialog(connection, networkHandlers, resetCredentials, onLogin); + const errorDetails = useErrorDetails(dialog.authException); + + useAutoLoad(DatabaseCredentialsAuthDialog, dialog); + + return ( + <> + +
+ + + +
+
+ + + {dialog.authException && ( + + )} + + + + ); +}); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx similarity index 89% rename from webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx rename to webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx index 67dc57ac5d..79b537bf54 100644 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/DatabaseCredentialsAuthDialogFooter.tsx @@ -32,7 +32,7 @@ export interface Props { className?: string; } -export const DBAuthDialogFooter = observer>(function DBAuthDialogFooter({ +export const DatabaseCredentialsAuthDialogFooter = observer>(function DatabaseCredentialsAuthDialogFooter({ isAuthenticating, onLogin, className, diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/useDatabaseAuthDialog.ts b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts similarity index 94% rename from webapp/packages/plugin-connections/src/DatabaseAuthDialog/useDatabaseAuthDialog.ts rename to webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts index fdb55bcb03..06c9378c10 100644 --- a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/useDatabaseAuthDialog.ts +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseCredentialsAuthDialog/useDatabaseCredentialsAuthDialog.ts @@ -21,7 +21,7 @@ import { useService } from '@cloudbeaver/core-di'; import { NetworkHandlerAuthType } from '@cloudbeaver/core-sdk'; import type { ILoadableState } from '@cloudbeaver/core-utils'; -import type { IConnectionAuthenticationConfig } from '../ConnectionAuthentication/IConnectionAuthenticationConfig'; +import type { IConnectionAuthenticationConfig } from '../../ConnectionAuthentication/IConnectionAuthenticationConfig'; interface IState extends ILoadableState { readonly authModelId: string | null; @@ -38,7 +38,12 @@ interface IState extends ILoadableState { getConfig: () => ConnectionInitConfig; } -export function useDatabaseAuthDialog(key: IConnectionInfoParams, networkHandlers: string[], resetCredentials?: boolean, onInit?: () => void) { +export function useDatabaseCredentialsAuthDialog( + key: IConnectionInfoParams, + networkHandlers: string[], + resetCredentials?: boolean, + onInit?: () => void, +) { const connectionInfoResource = useService(ConnectionInfoResource); const dbDriverResource = useService(DBDriverResource); diff --git a/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx new file mode 100644 index 0000000000..c5c09f4125 --- /dev/null +++ b/webapp/packages/plugin-connections/src/DatabaseAuthDialog/DatabaseSecretAuthDialog/DatabaseSecretAuthDialog.tsx @@ -0,0 +1,88 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; + +import { + CommonDialogBody, + CommonDialogFooter, + ExceptionMessage, + Group, + ItemList, + ListItem, + ListItemName, + Loader, + useObservableRef, + useResource, +} from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections'; + +interface Props { + connectionKey: IConnectionInfoParams; + onLogin?: () => void; +} + +export const DatabaseSecretAuthDialog = observer(function DatabaseSecretAuthDialog({ connectionKey, onLogin }) { + const connectionInfoLoader = useResource(DatabaseSecretAuthDialog, ConnectionInfoResource, { + key: connectionKey, + includes: ['includeAuthNeeded', 'includeSharedSecrets', 'includeNetworkHandlersConfig', 'includeCredentialsSaved'], + }); + const state = useObservableRef( + () => ({ + exception: null as Error | null, + authenticating: false, + }), + { + exception: observable.ref, + authenticating: observable.ref, + }, + false, + ); + const secrets = connectionInfoLoader.data?.sharedSecrets || []; + + async function handleSecretSelect(secretId: string) { + try { + state.authenticating = true; + await connectionInfoLoader.resource.init({ + ...connectionKey, + selectedSecretId: secretId, + }); + state.exception = null; + onLogin?.(); + } catch (exception: any) { + state.exception = exception; + } finally { + state.authenticating = false; + } + } + + if (state.authenticating) { + return ; + } + + return ( + <> + + + {secrets.map(secret => ( + handleSecretSelect(secret.secretId)}> + {secret.displayName} + + ))} + + + {state.exception && ( + + + + + + )} + + ); +}); diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index a0b4ace7f8..272bf0747f 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -8,8 +8,8 @@ import { action, makeObservable, observable } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; -import { ConnectionInfoResource, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; +import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionsManagerService, createConnectionParam, IConnectionInfoParams } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -20,11 +20,11 @@ import type { ConnectionConfig } from '@cloudbeaver/core-sdk'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; -import { ConnectionAuthService } from '../ConnectionAuthService'; import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService'; import { ConnectionFormState } from '../ConnectionForm/ConnectionFormState'; import type { IConnectionFormState } from '../ConnectionForm/IConnectionFormProps'; -import { PublicConnectionForm } from './PublicConnectionForm'; + +const PublicConnectionForm = importLazyComponent(() => import('./PublicConnectionForm').then(m => m.PublicConnectionForm)); const formGetter = () => PublicConnectionForm; @@ -38,7 +38,7 @@ export class PublicConnectionFormService { private readonly optionsPanelService: OptionsPanelService, private readonly connectionFormService: ConnectionFormService, private readonly connectionInfoResource: ConnectionInfoResource, - private readonly connectionAuthService: ConnectionAuthService, + private readonly connectionsManagerService: ConnectionsManagerService, private readonly userInfoResource: UserInfoResource, private readonly authenticationService: AuthenticationService, private readonly projectsService: ProjectsService, @@ -203,7 +203,7 @@ export class PublicConnectionFormService { try { await this.connectionInfoResource.close(connectionKey); - await this.connectionAuthService.auth(connectionKey); + await this.connectionsManagerService.requireConnection(connectionKey); } catch (exception: any) { this.notificationService.logException(exception, 'connections_public_connection_edit_reconnect_failed'); } diff --git a/webapp/packages/plugin-connections/src/index.ts b/webapp/packages/plugin-connections/src/index.ts index f2e9d6b180..9d5de586c7 100644 --- a/webapp/packages/plugin-connections/src/index.ts +++ b/webapp/packages/plugin-connections/src/index.ts @@ -7,6 +7,7 @@ export * from './ConnectionForm/DriverProperties/ConnectionDriverPropertiesTabSe export * from './ConnectionForm/SSH/ConnectionSSHTabService'; export * from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; export * from './ConnectionForm/Contexts/connectionConfigContext'; +export * from './ConnectionForm/Contexts/connectionCredentialsStateContext'; export * from './ConnectionForm/ConnectionFormBaseActionsLoader'; export * from './ConnectionForm/connectionFormConfigureContext'; export * from './ConnectionForm/ConnectionFormLoader'; @@ -14,6 +15,8 @@ export * from './ConnectionForm/ConnectionFormService'; export * from './ConnectionForm/ConnectionFormState'; export * from './ConnectionForm/IConnectionFormProps'; export * from './ConnectionForm/useConnectionFormState'; +export * from './ConnectionForm/SharedCredentials/CONNECTION_FORM_SHARED_CREDENTIALS_TAB_ID'; +export * from './ConnectionForm/ConnectionAuthModelCredentials/ConnectionAuthModelCredentialsForm'; export * from './ContextMenu/MENU_CONNECTION_VIEW'; export * from './ContextMenu/MENU_CONNECTIONS'; export * from './PublicConnectionForm/PublicConnectionFormService'; diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 98b3b99594..6c87ad975f 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -20,4 +20,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], ['connections_public_connection_ssl_enable', 'Enable SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + ['plugin_connections_connection_auth_secret_description', 'Please select credentials provided by one of your teams'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index c13c7bda6c..f95e5aceee 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -22,4 +22,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], ['connections_public_connection_ssl_enable', 'Enable SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + ['plugin_connections_connection_auth_secret_description', 'Please select credentials provided by one of your teams'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index 711186564a..9ba585075f 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -19,4 +19,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Показывать подключения только администраторам'], ['connections_public_connection_ssl_enable', 'Включить SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'Вы можете указать учетные данные в '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'во вкладке "Учетные данные"'], + ['plugin_connections_connection_auth_secret_description', 'Выберете учетные данные, предоставленные одной из ваших команд'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index e8c876924c..ef31d2598f 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -19,4 +19,8 @@ export default [ ['settings_connections_hide_connections_view_description', 'Show connections to admins only'], ['connections_public_connection_ssl_enable', 'Enable SSL'], + + ['plugin_connections_connection_form_shared_credentials_manage_info', 'You can manage credentials in the '], + ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Credentials tab'], + ['plugin_connections_connection_auth_secret_description', 'Please select credentials provided by one of your teams'], ]; diff --git a/webapp/packages/plugin-connections/src/manifest.ts b/webapp/packages/plugin-connections/src/manifest.ts index 4afa470555..ccdfe2fe77 100644 --- a/webapp/packages/plugin-connections/src/manifest.ts +++ b/webapp/packages/plugin-connections/src/manifest.ts @@ -13,13 +13,13 @@ import { ConnectionDriverPropertiesTabService } from './ConnectionForm/DriverPro import { ConnectionOptionsTabService } from './ConnectionForm/Options/ConnectionOptionsTabService'; import { ConnectionOriginInfoTabService } from './ConnectionForm/OriginInfo/ConnectionOriginInfoTabService'; import { ConnectionSSHTabService } from './ConnectionForm/SSH/ConnectionSSHTabService'; +import { ConnectionSSLTabService } from './ConnectionForm/SSL/ConnectionSSLTabService'; import { ConnectionMenuBootstrap } from './ContextMenu/ConnectionMenuBootstrap'; import { LocaleService } from './LocaleService'; import { ConnectionFoldersBootstrap } from './NavNodes/ConnectionFoldersBootstrap'; import { PluginBootstrap } from './PluginBootstrap'; import { PluginConnectionsSettingsService } from './PluginConnectionsSettingsService'; import { PublicConnectionFormService } from './PublicConnectionForm/PublicConnectionFormService'; -import { ConnectionSSLTabService } from './ConnectionForm/SSL/ConnectionSSLTabService'; export const connectionPlugin: PluginManifest = { info: { diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx index a8fa44907b..30bae2860f 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuFilter/FilterCustomValueDialog.tsx @@ -27,7 +27,7 @@ import style from './FilterCustomValueDialog.m.css'; interface IPayload { inputTitle: string; - defaultValue: string | number; + defaultValue: string; } export const FilterCustomValueDialog: DialogComponent = observer(function FilterCustomValueDialog({ @@ -39,7 +39,7 @@ export const FilterCustomValueDialog: DialogComponent const styles = useS(style); const inputRef = useRef(null); - const [value, setValue] = useState(payload.defaultValue); + const [value, setValue] = useState(payload.defaultValue); const handleApply = useCallback(() => resolveDialog(value), [value, resolveDialog]); const translate = useTranslate(); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts index 487f4e66e7..8e47e20604 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts @@ -21,9 +21,9 @@ import { DATA_EDITOR_SETTINGS_GROUP } from './DATA_EDITOR_SETTINGS_GROUP'; const defaultSettings = schema.object({ disableEdit: schema.coerce.boolean().default(false), disableCopyData: schema.coerce.boolean().default(false), - fetchMin: schema.coerce.number().default(100), - fetchMax: schema.coerce.number().default(5000), - fetchDefault: schema.coerce.number().default(200), + fetchMin: schema.coerce.number().min(10).default(100), + fetchMax: schema.coerce.number().min(10).default(5000), + fetchDefault: schema.coerce.number().min(10).default(200), }); export type DataViewerSettings = schema.infer; diff --git a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx index cb7f4ecb62..dd4d045432 100644 --- a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx +++ b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx @@ -6,11 +6,11 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import styled, { css } from 'reshadow'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { TextPlaceholder, useExecutor, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; +import { Loader, TextPlaceholder, useExecutor, useStyles, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { BASE_TAB_STYLES, ITabData, TabPanel, TabsBox } from '@cloudbeaver/core-ui'; import { CaptureView } from '@cloudbeaver/core-view'; @@ -47,6 +47,7 @@ export const NavigationTabsBar = observer(function NavigationTabsBar({ cl // TODO: we get exception when after closing the restored page trying to open another // it's related to hooks order and state restoration const style = useStyles(BASE_TAB_STYLES, styles); + const [restoring, setRestoring] = useState(false); const translate = useTranslate(); const handleSelect = useCallback((tabId: string) => navigation.selectTab(tabId), [navigation]); @@ -57,7 +58,12 @@ export const NavigationTabsBar = observer(function NavigationTabsBar({ cl } async function restoreTabs() { - await navigation.restoreTabs(); + setRestoring(true); + try { + await navigation.restoreTabs(); + } finally { + setRestoring(false); + } } function handleTabChange(tab: ITabData) { @@ -85,24 +91,26 @@ export const NavigationTabsBar = observer(function NavigationTabsBar({ cl return styled(style)( - ( - - ))} - tabList={navigation.tabIdList} - style={styles} - tabIndex={0} - autoSelect - enabledBaseActions - onChange={handleTabChange} - > - {navigation.tabIdList.map(tabId => ( - - {() => } - - ))} - + + ( + + ))} + tabList={navigation.tabIdList} + style={styles} + tabIndex={0} + autoSelect + enabledBaseActions + onChange={handleTabChange} + > + {navigation.tabIdList.map(tabId => ( + + {() => } + + ))} + + , ); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index a78d9cd38b..a0385f972b 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -40,7 +40,7 @@ export class ObjectPropertyTableFooterService { isPresent(context) { return context.contextType === ObjectPropertyTableFooterService.objectPropertyContextType; }, - isHidden: () => this.navTreeSettingsService.settings.getValue('deleting'), + isHidden: () => !this.navTreeSettingsService.settings.getValue('deleting'), isDisabled: context => { if (context.data.tableState.selectedList.length === 0) { return true; diff --git a/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts b/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts index 38c60dd002..bad5d7c8e4 100644 --- a/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-resource-manager-scripts/src/PluginBootstrap.ts @@ -5,6 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource'; import { ServerConfigResource } from '@cloudbeaver/core-root'; @@ -13,9 +14,10 @@ import { ActionService, DATA_CONTEXT_MENU, menuExtractItems, MenuService } from import { MENU_TOOLS } from '@cloudbeaver/plugin-tools-panel'; import { ACTION_RESOURCE_MANAGER_SCRIPTS } from './Actions/ACTION_RESOURCE_MANAGER_SCRIPTS'; -import { ResourceManagerScripts } from './ResourceManagerScripts'; import { ResourceManagerScriptsService } from './ResourceManagerScriptsService'; +const ResourceManagerScripts = importLazyComponent(() => import('./ResourceManagerScripts').then(m => m.ResourceManagerScripts)); + @injectable() export class PluginBootstrap extends Bootstrap { constructor( @@ -57,9 +59,12 @@ export class PluginBootstrap extends Bootstrap { isActionApplicable: (context, action) => [ACTION_RESOURCE_MANAGER_SCRIPTS].includes(action), isHidden: () => !this.resourceManagerScriptsService.enabled, isChecked: () => this.resourceManagerScriptsService.active, - getLoader: (context, action) => { - return getCachedDataResourceLoaderState(this.serverConfigResource, undefined, undefined); - }, + getLoader: () => + getCachedDataResourceLoaderState( + this.serverConfigResource, + () => undefined, + () => undefined, + ), handler: (context, action) => { switch (action) { case ACTION_RESOURCE_MANAGER_SCRIPTS: { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx index 33631e31d0..645fd6e7db 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTab.tsx @@ -16,7 +16,13 @@ import { useService } from '@cloudbeaver/core-di'; import { ITabData, Tab, TabIcon, TabTitle } from '@cloudbeaver/core-ui'; import { CaptureViewContext } from '@cloudbeaver/core-view'; import type { TabHandlerTabComponent } from '@cloudbeaver/plugin-navigation-tabs'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName, ISqlEditorTabState, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; +import { + DATA_CONTEXT_SQL_EDITOR_STATE, + ESqlDataSourceFeatures, + getSqlEditorName, + ISqlEditorTabState, + SqlDataSourceService, +} from '@cloudbeaver/plugin-sql-editor'; import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB'; import sqlEditorTabStyles from './SqlEditorTab.m.css'; @@ -24,13 +30,13 @@ import sqlEditorTabStyles from './SqlEditorTab.m.css'; export const SqlEditorTab: TabHandlerTabComponent = observer(function SqlEditorTab({ tab, onSelect, onClose, style }) { const viewContext = useContext(CaptureViewContext); const tabMenuContext = useDataContext(viewContext); - + tabMenuContext.set(DATA_CONTEXT_SQL_EDITOR_TAB, true); tabMenuContext.set(DATA_CONTEXT_SQL_EDITOR_STATE, tab.handlerState); - + const sqlDataSourceService = useService(SqlDataSourceService); const connectionInfo = useService(ConnectionInfoResource); - + const translate = useTranslate(); const dataSource = sqlDataSourceService.get(tab.handlerState.editorId); @@ -44,6 +50,7 @@ export const SqlEditorTab: TabHandlerTabComponent = observer const name = getSqlEditorName(tab.handlerState, dataSource, connection); const icon = dataSource?.icon ?? '/icons/sql_script_m.svg'; const saved = dataSource?.isSaved !== false; + const isScript = dataSource?.hasFeature(ESqlDataSourceFeatures.script); const isReadonly = Boolean(dataSource?.isReadonly()); const hasUnsavedMark = !saved && !isReadonly; @@ -54,7 +61,9 @@ export const SqlEditorTab: TabHandlerTabComponent = observer {name} - {isReadonly && } + {isReadonly && isScript && ( + + )} {hasUnsavedMark && } , ); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx index a65d27163e..590fd2cbd3 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx @@ -23,7 +23,15 @@ export const ChangePassword = observer(function ChangePassword() {
{translate('plugin_user_profile_authentication_change_password')} - value.trim()} small required> + value?.trim() ?? ''} + small + required + > {translate('plugin_user_profile_authentication_change_password_current_password')} value.trim()} + mapValue={(value?: string) => value?.trim() ?? ''} small required > {translate('plugin_user_profile_authentication_change_password_new_password')} - value.trim()} small required> + value?.trim() ?? ''} + small + required + > {translate('plugin_user_profile_authentication_change_password_repeat_password')} -