diff --git a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf index f65b0eee16..4102f85217 100644 --- a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf +++ b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf @@ -23,7 +23,13 @@ enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:true}", maxFailedLogin: "${CLOUDBEAVER_MAX_FAILED_LOGINS:10}", minimumLoginTimeout: "${CLOUDBEAVER_MINIMUM_LOGIN_TIMEOUT:1}", - blockLoginPeriod: "${CLOUDBEAVER_BLOCK_PERIOD:300}" + blockLoginPeriod: "${CLOUDBEAVER_BLOCK_PERIOD:300}", + passwordPolicy: { + minLength: "${CLOUDBEAVER_POLICY_MIN_LENGTH:8}", + requireMixedCase: "${CLOUDBEAVER_POLICY_REQUIRE_MIXED_CASE:true}", + minNumberCount: "${CLOUDBEAVER_POLICY_MIN_NUMBER_COUNT:1}", + minSymbolCount: "${CLOUDBEAVER_POLICY_MIN_SYMBOL_COUNT:0}" + } }, database: { diff --git a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf index 67427c38c1..aceb52c885 100644 --- a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf +++ b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf @@ -27,6 +27,18 @@ maxConnections: 100, validationQuery: "SELECT 1" } + }, + sm: { + enableBruteForceProtection: "${CLOUDBEAVER_BRUTE_FORCE_PROTECTION_ENABLED:true}", + maxFailedLogin: "${CLOUDBEAVER_MAX_FAILED_LOGINS:10}", + minimumLoginTimeout: "${CLOUDBEAVER_MINIMUM_LOGIN_TIMEOUT:1}", + blockLoginPeriod: "${CLOUDBEAVER_BLOCK_PERIOD:300}", + passwordPolicy: { + minLength: "${CLOUDBEAVER_POLICY_MIN_LENGTH:8}", + requireMixedCase: "${CLOUDBEAVER_POLICY_REQUIRE_MIXED_CASE:true}", + minNumberCount: "${CLOUDBEAVER_POLICY_MIN_NUMBER_COUNT:1}", + minSymbolCount: "${CLOUDBEAVER_POLICY_MIN_SYMBOL_COUNT:0}" + } } }, diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 0e998720b4..0b2124d5e9 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -99,6 +99,13 @@ type WebServiceConfig { bundleVersion: String! } +type PasswordPolicyConfig @since(version: "23.3.3") { + minLength: Int! + minNumberCount: Int! + minSymbolCount: Int! + requireMixedCase: Boolean! +} + type ProductInfo { id: ID! version: String! @@ -152,6 +159,7 @@ type ServerConfig { defaultNavigatorSettings: NavigatorSettings! disabledDrivers: [ID!]! resourceQuotas: Object! + passwordPolicyConfiguration: PasswordPolicyConfig! @since(version: "23.3.3") } type SessionInfo { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index bc2eda8600..ea429a9529 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -20,6 +20,7 @@ import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.service.security.PasswordPolicyConfiguration; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.registry.language.PlatformLanguageDescriptor; @@ -217,4 +218,9 @@ public String getDefaultAuthRole() { public String getDefaultUserTeam() { return application.getAppConfiguration().getDefaultUserTeam(); } + + @Property + public PasswordPolicyConfiguration getPasswordPolicyConfiguration() { + return application.getSecurityManagerConfiguration().getPasswordPolicyConfiguration(); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index cf13487e0f..b931441a47 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -22,6 +22,7 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.CBAuthConstants; import io.cloudbeaver.auth.NoAuthCredentialsProvider; +import io.cloudbeaver.service.security.PasswordPolicyConfiguration; import io.cloudbeaver.model.app.BaseWebApplication; import io.cloudbeaver.model.app.WebAuthApplication; import io.cloudbeaver.model.app.WebAuthConfiguration; @@ -231,6 +232,10 @@ public Map getProductConfiguration() { return productConfiguration; } + public SMControllerConfiguration getSecurityManagerConfiguration() { + return securityManagerConfiguration; + } + public SMAdminController getSecurityController() { return securityController; } @@ -592,7 +597,7 @@ protected void parseConfiguration(Map configProps) throws DBExce enableSecurityManager); //SM config gson.fromJson( - gson.toJsonTree(JSONUtils.getObject(serverConfig, CBConstants.PARAM_SM_CONFIGURATION)), + gson.toJson(JSONUtils.getObject(serverConfig, CBConstants.PARAM_SM_CONFIGURATION)), SMControllerConfiguration.class ); // App config @@ -772,11 +777,14 @@ protected GsonBuilder getGsonBuilder() { InstanceCreator appConfigCreator = type -> appConfiguration; InstanceCreator navSettingsCreator = type -> (DataSourceNavigatorSettings) appConfiguration.getDefaultNavigatorSettings(); InstanceCreator smConfigCreator = type -> securityManagerConfiguration; + InstanceCreator smPasswordPoliceConfigCreator = + type -> securityManagerConfiguration.getPasswordPolicyConfiguration(); return new GsonBuilder() .setLenient() .registerTypeAdapter(CBAppConfig.class, appConfigCreator) .registerTypeAdapter(DataSourceNavigatorSettings.class, navSettingsCreator) - .registerTypeAdapter(SMControllerConfiguration.class, smConfigCreator); + .registerTypeAdapter(SMControllerConfiguration.class, smConfigCreator) + .registerTypeAdapter(PasswordPolicyConfiguration.class, smPasswordPoliceConfigCreator); } protected void readAdditionalConfiguration(Map rootConfig) throws DBException { @@ -1042,6 +1050,7 @@ protected Map collectConfigurationProperties( } serverConfigProperties.put(CBConstants.PARAM_DB_CONFIGURATION, databaseConfigProperties); } + savePasswordPolicyConfig(originServerConfig, serverConfigProperties); } { var appConfigProperties = new LinkedHashMap(); @@ -1151,6 +1160,30 @@ protected Map collectConfigurationProperties( return rootConfig; } + private void savePasswordPolicyConfig(Map originServerConfig, LinkedHashMap serverConfigProperties) { + // save password policy configuration + var passwordPolicyProperties = new LinkedHashMap(); + + var oldRuntimePasswordPolicyConfig = JSONUtils.getObject( + JSONUtils.getObject(originServerConfig, CBConstants.PARAM_SM_CONFIGURATION), + CBConstants.PARAM_PASSWORD_POLICY_CONFIGURATION + ); + Gson gson = getGson(); + Map passwordPolicyConfig = gson.fromJson( + gson.toJsonTree(securityManagerConfiguration.getPasswordPolicyConfiguration()), + JSONUtils.MAP_TYPE_TOKEN + ); + if (!CommonUtils.isEmpty(passwordPolicyConfig) && !isDistributed()) { + for (Map.Entry mp : passwordPolicyConfig.entrySet()) { + copyConfigValue(oldRuntimePasswordPolicyConfig, passwordPolicyProperties, mp.getKey(), mp.getValue()); + } + serverConfigProperties.put( + CBConstants.PARAM_SM_CONFIGURATION, + Map.of(CBConstants.PARAM_PASSWORD_POLICY_CONFIGURATION, passwordPolicyProperties) + ); + } + } + //////////////////////////////////////////////////////////////////////// // License management diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java index 9eb64d24c0..7e35aba999 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java @@ -50,6 +50,7 @@ public class CBConstants { public static final String PARAM_DEVEL_MODE = "develMode"; public static final String PARAM_SECURITY_MANAGER = "enableSecurityManager"; public static final String PARAM_SM_CONFIGURATION = "sm"; + public static final String PARAM_PASSWORD_POLICY_CONFIGURATION = "passwordPolicy"; public static final int DEFAULT_SERVER_PORT = 8080; //public static final String DEFAULT_SERVER_NAME = "CloudBeaver Web Server"; diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/PasswordPolicyConfiguration.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/PasswordPolicyConfiguration.java new file mode 100644 index 0000000000..28076d7174 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/PasswordPolicyConfiguration.java @@ -0,0 +1,50 @@ +/* + * 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.service.security; + +import org.jkiss.dbeaver.model.meta.Property; + +public class PasswordPolicyConfiguration { + private static final int DEFAULT_MIN_LENGTH = 8; + private static final int DEFAULT_MIN_DIGITS = 1; + private static final int DEFAULT_MIN_SPECIAL_CHARACTERS = 0; + private static final boolean DEFAULT_REQUIRES_UPPER_LOWER_CASE = true; + private int minLength = DEFAULT_MIN_LENGTH; + private int minNumberCount = DEFAULT_MIN_DIGITS; + private int minSymbolCount = DEFAULT_MIN_SPECIAL_CHARACTERS; + private boolean requireMixedCase = DEFAULT_REQUIRES_UPPER_LOWER_CASE; + + @Property + public int getMinLength() { + return minLength; + } + + @Property + public int getMinNumberCount() { + return minNumberCount; + } + + @Property + public int getMinSymbolCount() { + return minSymbolCount; + } + + @Property + public boolean isRequireMixedCase() { + return requireMixedCase; + } +} diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java index 26fb64585e..70a704faf1 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/SMControllerConfiguration.java @@ -36,6 +36,7 @@ public class SMControllerConfiguration { private int maxFailedLogin = DEFAULT_MAX_FAILED_LOGIN; private int minimumLoginTimeout = DEFAULT_MINIMUM_LOGIN_TIMEOUT; private int blockLoginPeriod = DEFAULT_BLOCK_LOGIN_PERIOD; + private final PasswordPolicyConfiguration passwordPolicy = new PasswordPolicyConfiguration(); public int getAccessTokenTtl() { return accessTokenTtl; @@ -92,4 +93,8 @@ public void setMinimumLoginTimeout(int minimumTimeout) { public void setBlockLoginPeriod(int blockPeriod) { this.blockLoginPeriod = blockPeriod; } + + public PasswordPolicyConfiguration getPasswordPolicyConfiguration() { + return passwordPolicy; + } } diff --git a/webapp/packages/core-authentication/src/PasswordPolicyService.ts b/webapp/packages/core-authentication/src/PasswordPolicyService.ts new file mode 100644 index 0000000000..4c19107734 --- /dev/null +++ b/webapp/packages/core-authentication/src/PasswordPolicyService.ts @@ -0,0 +1,75 @@ +/* + * 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 { computed, makeObservable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import type { PasswordPolicyConfig } from '@cloudbeaver/core-sdk'; + +const DEFAULT_PASSWORD_POLICY: PasswordPolicyConfig = { + minLength: 8, + minNumberCount: 0, + minSymbolCount: 0, + requireMixedCase: false, +}; + +type ValidationResult = { isValid: true; errorMessage: null } | { isValid: false; errorMessage: string }; + +@injectable() +export class PasswordPolicyService { + get config(): PasswordPolicyConfig { + return { + minLength: this.serverConfigResource.data?.passwordPolicyConfiguration?.minLength || DEFAULT_PASSWORD_POLICY.minLength, + minNumberCount: this.serverConfigResource.data?.passwordPolicyConfiguration?.minNumberCount || DEFAULT_PASSWORD_POLICY.minNumberCount, + minSymbolCount: this.serverConfigResource.data?.passwordPolicyConfiguration?.minSymbolCount || DEFAULT_PASSWORD_POLICY.minSymbolCount, + requireMixedCase: this.serverConfigResource.data?.passwordPolicyConfiguration?.requireMixedCase || DEFAULT_PASSWORD_POLICY.requireMixedCase, + }; + } + + constructor(private readonly serverConfigResource: ServerConfigResource, private readonly localizationService: LocalizationService) { + makeObservable(this, { + config: computed, + }); + } + + validatePassword(password: string): ValidationResult { + const trimmedPassword = password.trim(); + + if (trimmedPassword.length < this.config.minLength) { + return { + isValid: false, + errorMessage: this.localizationService.translate('core_authentication_password_policy_min_length', undefined, { min: this.config.minLength }), + }; + } + + if (this.config.requireMixedCase && !(/\p{Ll}/u.test(trimmedPassword) && /\p{Lu}/u.test(trimmedPassword))) { + return { isValid: false, errorMessage: this.localizationService.translate('core_authentication_password_policy_upper_lower_case') }; + } + + if ((trimmedPassword.match(/\d/g) || []).length < this.config.minNumberCount) { + return { + isValid: false, + errorMessage: this.localizationService.translate('core_authentication_password_policy_min_digits', undefined, { + min: this.config.minNumberCount, + }), + }; + } + + if ((trimmedPassword.match(/[!@#$%^&*(),.?":{}|<>]/g) || []).length < this.config.minSymbolCount) { + return { + isValid: false, + errorMessage: this.localizationService.translate('core_authentication_password_policy_min_special_characters', undefined, { + min: this.config.minSymbolCount, + }), + }; + } + + return { isValid: true, errorMessage: null }; + } +} diff --git a/webapp/packages/core-authentication/src/index.ts b/webapp/packages/core-authentication/src/index.ts index b2b7c7af17..a18575bacc 100644 --- a/webapp/packages/core-authentication/src/index.ts +++ b/webapp/packages/core-authentication/src/index.ts @@ -20,3 +20,5 @@ export * from './UsersResource'; export * from './TeamMetaParametersResource'; export * from './EAdminPermission'; export * from './AUTH_SETTINGS_GROUP'; +export * from './PasswordPolicyService'; +export * from './usePasswordPolicy'; diff --git a/webapp/packages/core-authentication/src/locales/en.ts b/webapp/packages/core-authentication/src/locales/en.ts index fd933c546c..4554669df2 100644 --- a/webapp/packages/core-authentication/src/locales/en.ts +++ b/webapp/packages/core-authentication/src/locales/en.ts @@ -2,4 +2,9 @@ export default [ ['settings_authentication', 'Authentication'], ['settings_authentication_disable_anonymous_access_name', 'Disable anonymous access'], ['settings_authentication_disable_anonymous_access_description', 'Disable anonymous access function'], + + ['core_authentication_password_policy_min_length', 'Password must be at least {arg:min} characters long'], + ['core_authentication_password_policy_upper_lower_case', 'Password must contain both upper and lower case letters'], + ['core_authentication_password_policy_min_digits', 'Password must contain at least {arg:min} digits'], + ['core_authentication_password_policy_min_special_characters', 'Password must contain at least {arg:min} special characters'], ]; diff --git a/webapp/packages/core-authentication/src/locales/it.ts b/webapp/packages/core-authentication/src/locales/it.ts index fd933c546c..4554669df2 100644 --- a/webapp/packages/core-authentication/src/locales/it.ts +++ b/webapp/packages/core-authentication/src/locales/it.ts @@ -2,4 +2,9 @@ export default [ ['settings_authentication', 'Authentication'], ['settings_authentication_disable_anonymous_access_name', 'Disable anonymous access'], ['settings_authentication_disable_anonymous_access_description', 'Disable anonymous access function'], + + ['core_authentication_password_policy_min_length', 'Password must be at least {arg:min} characters long'], + ['core_authentication_password_policy_upper_lower_case', 'Password must contain both upper and lower case letters'], + ['core_authentication_password_policy_min_digits', 'Password must contain at least {arg:min} digits'], + ['core_authentication_password_policy_min_special_characters', 'Password must contain at least {arg:min} special characters'], ]; diff --git a/webapp/packages/core-authentication/src/locales/ru.ts b/webapp/packages/core-authentication/src/locales/ru.ts index 7edfa853fa..51fffc3dbd 100644 --- a/webapp/packages/core-authentication/src/locales/ru.ts +++ b/webapp/packages/core-authentication/src/locales/ru.ts @@ -2,4 +2,9 @@ export default [ ['settings_authentication', 'Аутентификация'], ['settings_authentication_disable_anonymous_access_name', 'Отключить анонимный доступ'], ['settings_authentication_disable_anonymous_access_description', 'Отключить функцию анонимного доступа'], + + ['core_authentication_password_policy_min_length', 'Пароль должен быть не менее {arg:min} символов'], + ['core_authentication_password_policy_upper_lower_case', 'Пароль должен содержать как заглавные, так и строчные буквы'], + ['core_authentication_password_policy_min_digits', 'Пароль должен содержать не менее {arg:min} цифр'], + ['core_authentication_password_policy_min_special_characters', 'Пароль должен содержать не менее {arg:min} специальных символов'], ]; diff --git a/webapp/packages/core-authentication/src/locales/zh.ts b/webapp/packages/core-authentication/src/locales/zh.ts index fd933c546c..4554669df2 100644 --- a/webapp/packages/core-authentication/src/locales/zh.ts +++ b/webapp/packages/core-authentication/src/locales/zh.ts @@ -2,4 +2,9 @@ export default [ ['settings_authentication', 'Authentication'], ['settings_authentication_disable_anonymous_access_name', 'Disable anonymous access'], ['settings_authentication_disable_anonymous_access_description', 'Disable anonymous access function'], + + ['core_authentication_password_policy_min_length', 'Password must be at least {arg:min} characters long'], + ['core_authentication_password_policy_upper_lower_case', 'Password must contain both upper and lower case letters'], + ['core_authentication_password_policy_min_digits', 'Password must contain at least {arg:min} digits'], + ['core_authentication_password_policy_min_special_characters', 'Password must contain at least {arg:min} special characters'], ]; diff --git a/webapp/packages/core-authentication/src/manifest.ts b/webapp/packages/core-authentication/src/manifest.ts index b6ae2c3fc1..d61ab7a50e 100644 --- a/webapp/packages/core-authentication/src/manifest.ts +++ b/webapp/packages/core-authentication/src/manifest.ts @@ -16,6 +16,7 @@ import { AuthProvidersResource } from './AuthProvidersResource'; import { AuthRolesResource } from './AuthRolesResource'; import { AuthSettingsService } from './AuthSettingsService'; import { LocaleService } from './LocaleService'; +import { PasswordPolicyService } from './PasswordPolicyService'; import { TeamMetaParametersResource } from './TeamMetaParametersResource'; import { TeamsManagerService } from './TeamsManagerService'; import { TeamsResource } from './TeamsResource'; @@ -47,6 +48,7 @@ export const coreAuthenticationManifest: PluginManifest = { UserConfigurationBootstrap, AuthRolesResource, TeamMetaParametersResource, + PasswordPolicyService, LocaleService, ], }; diff --git a/webapp/packages/core-authentication/src/usePasswordPolicy.ts b/webapp/packages/core-authentication/src/usePasswordPolicy.ts new file mode 100644 index 0000000000..fed5237589 --- /dev/null +++ b/webapp/packages/core-authentication/src/usePasswordPolicy.ts @@ -0,0 +1,22 @@ +/* + * 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 { useCustomInputValidation } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; + +import { PasswordPolicyService } from './PasswordPolicyService'; + +export function usePasswordPolicy() { + const passwordPolicyService = useService(PasswordPolicyService); + + const ref = useCustomInputValidation(value => { + const validation = passwordPolicyService.validatePassword(value); + return validation.isValid ? null : validation.errorMessage; + }); + + return ref; +} diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index 168c131608..b3be79531e 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -149,6 +149,10 @@ export class ServerConfigResource extends CachedDataResource) = releaseTime: 'July 11, 2022', licenseInfo: '', }, + passwordPolicyConfiguration: { + minLength: 8, + minNumberCount: 0, + minSymbolCount: 0, + requireMixedCase: false, + }, }, }); diff --git a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql index da4ec7912d..b88cfc9032 100644 --- a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql +++ b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql @@ -52,5 +52,11 @@ query serverConfig { releaseTime licenseInfo } + passwordPolicyConfiguration { + minLength + minNumberCount + minSymbolCount + requireMixedCase + } } } diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index 345f7ddfe4..090ab61d5f 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -153,7 +153,7 @@ export class ServerConfigurationService { const validation = contexts.getContext(serverConfigValidationContext); - return validation.getState(); + return validation.valid; } private readonly loadServerConfig: IExecutorHandler = async (data, contexts) => { @@ -200,7 +200,7 @@ export class ServerConfigurationService { private readonly save: IExecutorHandler = async (data, contexts) => { const validation = contexts.getContext(serverConfigValidationContext); - if (!validation.getState()) { + if (!validation.valid) { return; } @@ -221,8 +221,18 @@ export class ServerConfigurationService { private readonly ensureValidation: IExecutorHandler = (data, contexts) => { const validation = contexts.getContext(serverConfigValidationContext); - if (!validation.getState()) { + if (!validation.valid) { ExecutorInterrupter.interrupt(contexts); + + if (validation.messages.length > 0) { + this.notificationService.notify( + { + title: 'administration_configuration_wizard_step_validation_message', + message: validation.messages.join('\n'), + }, + validation.valid ? ENotificationType.Info : ENotificationType.Error, + ); + } this.done = false; } else { this.done = true; @@ -295,21 +305,27 @@ export class ServerConfigurationService { } export interface IValidationStatusContext { - getState: () => boolean; + valid: boolean; + messages: string[]; invalidate: () => void; + info: (message: string) => void; + error: (message: string) => void; } export function serverConfigValidationContext(): IValidationStatusContext { - let state = true; - - const invalidate = () => { - state = false; - }; - const getState = () => state; - return { - getState, - invalidate, + valid: true, + messages: [], + invalidate() { + this.valid = false; + }, + info(message: string) { + this.messages.push(message); + }, + error(message: string) { + this.messages.push(message); + this.valid = false; + }, }; } diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts index 10887d7624..b2ec88b42a 100644 --- a/webapp/packages/plugin-administration/src/locales/en.ts +++ b/webapp/packages/plugin-administration/src/locales/en.ts @@ -65,6 +65,8 @@ export default [ ['administration_configuration_wizard_configuration_navigator_show_system_objects', 'System Objects'], ['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Utility Objects'], + ['administration_configuration_wizard_step_validation_message', 'Failed to proceed to the next step'], + ['administration_configuration_wizard_finish', 'Confirmation'], ['administration_configuration_wizard_finish_step_description', 'Confirmation'], ['administration_configuration_wizard_finish_title', 'That is almost it.'], diff --git a/webapp/packages/plugin-administration/src/locales/it.ts b/webapp/packages/plugin-administration/src/locales/it.ts index 2fcc1d55e8..1f70630398 100644 --- a/webapp/packages/plugin-administration/src/locales/it.ts +++ b/webapp/packages/plugin-administration/src/locales/it.ts @@ -70,6 +70,8 @@ export default [ ['administration_configuration_wizard_configuration_navigator_show_system_objects', 'Oggetti di Sistema'], ['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Oggetti di UtilitàUtility Objects'], + ['administration_configuration_wizard_step_validation_message', 'Failed to proceed to the next step'], + ['administration_configuration_wizard_finish', 'Conferma'], ['administration_configuration_wizard_finish_step_description', 'Conferma'], ['administration_configuration_wizard_finish_title', 'Ci siamo quasi.'], diff --git a/webapp/packages/plugin-administration/src/locales/ru.ts b/webapp/packages/plugin-administration/src/locales/ru.ts index aaed836b51..1a506514bd 100644 --- a/webapp/packages/plugin-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-administration/src/locales/ru.ts @@ -26,6 +26,8 @@ export default [ 'Все новые подключения, созданные пользователем, будут иметь только базовую информацию в дереве навигации', ], + ['administration_configuration_wizard_step_validation_message', 'Не удалось перейти к следующему шагу'], + ['administration_configuration_wizard_configuration_security', 'Безопасность'], ['administration_configuration_wizard_configuration_security_admin_credentials', 'Позволить сохранять приватные данные'], ['administration_configuration_wizard_configuration_security_public_credentials', 'Позволить сохранять приватные данные для пользователей'], diff --git a/webapp/packages/plugin-administration/src/locales/zh.ts b/webapp/packages/plugin-administration/src/locales/zh.ts index 1dc16e6cb9..a815b51f49 100644 --- a/webapp/packages/plugin-administration/src/locales/zh.ts +++ b/webapp/packages/plugin-administration/src/locales/zh.ts @@ -52,6 +52,8 @@ export default [ ['administration_configuration_wizard_configuration_navigator_show_system_objects', '系统对象'], ['administration_configuration_wizard_configuration_navigator_show_utility_objects', '实用程序对象'], + ['administration_configuration_wizard_step_validation_message', 'Failed to proceed to the next step'], + ['administration_configuration_wizard_finish', '确认'], ['administration_configuration_wizard_finish_step_description', '确认'], ['administration_configuration_wizard_finish_title', '差不多就这样。'], diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts index 293767a2a2..7df18decf2 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource } from '@cloudbeaver/core-authentication'; +import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, PasswordPolicyService } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; @@ -27,6 +27,7 @@ export class ServerConfigurationAuthenticationBootstrap extends Bootstrap { private readonly authProvidersResource: AuthProvidersResource, private readonly serverConfigResource: ServerConfigResource, private readonly notificationService: NotificationService, + private readonly passwordPolicyService: PasswordPolicyService, ) { super(); } @@ -81,7 +82,12 @@ export class ServerConfigurationAuthenticationBootstrap extends Bootstrap { const validation = contexts.getContext(serverConfigValidationContext); if (!data.state.serverConfig.adminName || data.state.serverConfig.adminName.length < 6 || !data.state.serverConfig.adminPassword) { - validation.invalidate(); + return validation.invalidate(); + } + + const passwordValidation = this.passwordPolicyService.validatePassword(data.state.serverConfig.adminPassword); + if (!passwordValidation.isValid) { + validation.error(passwordValidation.errorMessage); } }; } diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx index aade7c157c..23698b5958 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfoCredentials.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, isLocalUser, UsersResource } from '@cloudbeaver/core-authentication'; +import { AUTH_PROVIDER_LOCAL_ID, AuthProvidersResource, isLocalUser, usePasswordPolicy, UsersResource } from '@cloudbeaver/core-authentication'; import { Container, GroupTitle, InputField, useCustomInputValidation, useResource, useTranslate } from '@cloudbeaver/core-blocks'; import { FormMode } from '@cloudbeaver/core-ui'; import { isValuesEqual } from '@cloudbeaver/core-utils'; @@ -33,6 +33,7 @@ export const UserFormInfoCredentials = observer(function UserFormInfoCred { active: tabSelected && editing }, ); const authProvidersResource = useResource(UserFormInfoCredentials, AuthProvidersResource, null); + const passwordPolicyRef = usePasswordPolicy(); let local = authProvidersResource.resource.isEnabled(AUTH_PROVIDER_LOCAL_ID); @@ -56,6 +57,7 @@ export const UserFormInfoCredentials = observer(function UserFormInfoCred {local && ( <> ({ @@ -42,6 +43,16 @@ export function useChangePassword(): IState { return this.config.password.length > 0 && this.config.oldPassword.length > 0 && this.config.repeatedPassword.length > 0; }, async changePassword() { + const validation = this.passwordPolicyService.validatePassword(this.config.password); + + if (!validation.isValid) { + this.notificationService.logError({ + title: 'plugin_user_profile_authentication_change_password_password_validation_error', + message: validation.errorMessage, + }); + return; + } + if (this.config.password !== this.config.repeatedPassword) { this.notificationService.logError({ title: 'plugin_user_profile_authentication_change_password_passwords_not_match' }); return; @@ -71,6 +82,6 @@ export function useChangePassword(): IState { changePassword: action.bound, resetConfig: action, }, - { usersResource, notificationService }, + { usersResource, notificationService, passwordPolicyService }, ); } diff --git a/webapp/packages/plugin-user-profile/src/locales/en.ts b/webapp/packages/plugin-user-profile/src/locales/en.ts index 0317590459..c0a5a26775 100644 --- a/webapp/packages/plugin-user-profile/src/locales/en.ts +++ b/webapp/packages/plugin-user-profile/src/locales/en.ts @@ -15,4 +15,5 @@ export default [ ['plugin_user_profile_authentication_change_password_success', 'Password was changed successfully'], ['plugin_user_profile_authentication_change_password_submit_label', 'Change'], ['plugin_user_profile_authentication_change_password_passwords_not_match', "Passwords don't match"], + ['plugin_user_profile_authentication_change_password_password_validation_error', 'Password validation failed'], ]; diff --git a/webapp/packages/plugin-user-profile/src/locales/it.ts b/webapp/packages/plugin-user-profile/src/locales/it.ts index b6e2d18a81..2c796e7aeb 100644 --- a/webapp/packages/plugin-user-profile/src/locales/it.ts +++ b/webapp/packages/plugin-user-profile/src/locales/it.ts @@ -15,4 +15,5 @@ export default [ ['plugin_user_profile_authentication_change_password_success', 'Password modificata con successo'], ['plugin_user_profile_authentication_change_password_submit_label', 'Modifica'], ['plugin_user_profile_authentication_change_password_passwords_not_match', 'Le Passwords non coincidono'], + ['plugin_user_profile_authentication_change_password_password_validation_error', 'Password validation failed'], ]; diff --git a/webapp/packages/plugin-user-profile/src/locales/ru.ts b/webapp/packages/plugin-user-profile/src/locales/ru.ts index e6c3f0dce4..ac5bc52bc8 100644 --- a/webapp/packages/plugin-user-profile/src/locales/ru.ts +++ b/webapp/packages/plugin-user-profile/src/locales/ru.ts @@ -15,4 +15,5 @@ export default [ ['plugin_user_profile_authentication_change_password_success', 'Пароль был успешно изменен'], ['plugin_user_profile_authentication_change_password_submit_label', 'Сменить'], ['plugin_user_profile_authentication_change_password_passwords_not_match', 'Пароли не совпадают'], + ['plugin_user_profile_authentication_change_password_password_validation_error', 'Валидация пароля не удалась'], ]; diff --git a/webapp/packages/plugin-user-profile/src/locales/zh.ts b/webapp/packages/plugin-user-profile/src/locales/zh.ts index e29fb5c32d..f1abcfe504 100644 --- a/webapp/packages/plugin-user-profile/src/locales/zh.ts +++ b/webapp/packages/plugin-user-profile/src/locales/zh.ts @@ -15,4 +15,5 @@ export default [ ['plugin_user_profile_authentication_change_password_success', '密码更改成功'], ['plugin_user_profile_authentication_change_password_submit_label', '更改'], ['plugin_user_profile_authentication_change_password_passwords_not_match', '密码不匹配'], + ['plugin_user_profile_authentication_change_password_password_validation_error', 'Password validation failed'], ];