Compare commits

...
Author SHA1 Message Date
Alexander SkoblikovandSychev Andrey 7bee81952d Hotfix bind session to ip (#3629)
* dbeaver/pro#6382 use select for bindSessionToIP setting instead of combobox

* dbeaver/pro#6382 bind session ip to dbeaver's hosts

* dbeaver/pro#6382 feat: add placeholder for bindToIp setting

* dbeaver/pro#6382 fix: defaultServerConfig

* dbeaver/pro#6382 refactor: remove l10n from CE

* dbeaver/pro#6382 feat: add bind session to ip combobox in server config

---------

Co-authored-by: Sychev Andrey <nukemoore@gmail.com>
2025-08-04 16:38:01 +02:00
Alexander Skoblikov 698dc8a312 Hotfix redirect ports (#3627)
* dbeaver/pro#6467 ignore default ports

* dbeaver/pro#6467 check port

* dbeaver/pro#6467 fix ip host parsing
2025-08-04 13:47:13 +02:00
22 changed files with 145 additions and 61 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
},
expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}",
bindSessionToIp: "${CLOUDBEAVER_BIND_SESSION_TO_IP:true}",
bindSessionToIp: "${CLOUDBEAVER_BIND_SESSION_TO_IP:disable}",
develMode: "${CLOUDBEAVER_DEVEL_MODE:false}",
enableSecurityManager: false,
@@ -33,6 +33,7 @@ public class CBConstants {
public static final String PARAM_SERVER_HOST = "serverHost";
public static final String PARAM_SERVER_NAME = "serverName";
public static final String PARAM_FORCE_HTTPS = "forceHttps";
public static final String PARAM_BIND_SESSION_TO_IP = "bindSessionToIp";
public static final String PARAM_SUPPORTED_HOSTS = "supportedHosts";
public static final String PARAM_SSL_CONFIGURATION_PATH = "sslConfigurationPath";
public static final String PARAM_CONTENT_ROOT = "contentRoot";
@@ -94,4 +95,7 @@ public class CBConstants {
public static final String ADMIN_AUTO_GRANT = "auto-grant";
public static final String HOST_LOCALHOST = "localhost";
public static final String HOST_127_0_0_1 = "127.0.0.1";
public static final String BIND_SESSION_ENABLE = "enable";
public static final String BIND_SESSION_DISABLE = "disable";
}
@@ -37,6 +37,7 @@ import org.jkiss.dbeaver.utils.GeneralUtils;
import org.jkiss.utils.CommonUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.*;
@@ -47,6 +48,11 @@ public class ServletAppUtils {
private static final String HEADER_FORWARDED_SCHEME = "X-Forwarded-Scheme";
private static final String HEADER_FORWARDED_HOST = "X-Forwarded-Host";
private static final Set<Integer> DEFAULT_PORTS = Set.of(
80,
443
);
private static final Log log = Log.getLog(ServletAppUtils.class);
@@ -309,7 +315,26 @@ public class ServletAppUtils {
if (!origin.endsWith(rootUri)) {
origin = origin + "/" + rootUri + "/";
}
return removeSideSlashes(origin);
origin = removeSideSlashes(origin);
URI uri = URI.create(origin);
if (DEFAULT_PORTS.contains(uri.getPort())) {
try {
origin = new URI(
uri.getScheme(),
uri.getUserInfo(),
uri.getHost(),
-1,
uri.getPath(),
uri.getQuery(),
uri.getFragment()
).toString();
} catch (URISyntaxException e) {
log.error("Failed to create URI without port", e);
}
}
return origin;
}
public static String getRootUrlFromUri(@NotNull URI uri) {
@@ -16,5 +16,7 @@ extend type ServerConfig {
forceHttps: Boolean! @since(version: "25.1.3")
"List of allowed hosts to use the application - if empty, all hosts are allowed"
supportedHosts: [String!]! @since(version: "25.1.3")
"Indicates whether the session should be linked to the user's IP address"
bindSessionToIp: String! @since(version: "25.1.4")
}
@@ -99,4 +99,10 @@ public class CBWebServerConfig extends WebServerConfig {
public List<String> getSupportedHosts() {
return cbApp.getServerConfiguration().getSupportedHosts();
}
@NotNull
@Property
public String getBindSessionToIp() {
return cbApp.getServerConfiguration().getBindSessionToIp();
}
}
@@ -55,7 +55,8 @@ public class CBServerConfig implements WebServerConfiguration {
@SerializedName("database")
private WebDatabaseConfig databaseConfiguration = new WebDatabaseConfig();
private String staticContent = "";
private boolean bindSessionToIp = true;
@NotNull
private String bindSessionToIp = CBConstants.BIND_SESSION_DISABLE;
public CBServerConfig() {
this.securityManagerConfiguration = createSecurityManagerConfiguration();
@@ -204,9 +205,17 @@ public class CBServerConfig implements WebServerConfiguration {
LinkedHashSet<String> uniqueHosts = new LinkedHashSet<>();
for (String host : availableHosts) {
try {
if (!host.startsWith("http://") && !host.startsWith("https://")) {
host = "http://" + host; // Default to HTTP if no scheme is provided to avoid uri parse exception
}
URI uri = URI.create(host);
String hostName = ServletAppUtils.removeSideSlashes(uri.getHost() != null ? uri.getHost() : host);
uniqueHosts.add(hostName);
StringBuilder hostNameBuilder = new StringBuilder();
hostNameBuilder.append(ServletAppUtils.removeSideSlashes(uri.getHost() != null ? uri.getHost() : host));
if (uri.getPort() > 0) {
hostNameBuilder.append(':')
.append(uri.getPort());
}
uniqueHosts.add(hostNameBuilder.toString());
} catch (Exception e) {
log.error("Invalid host URI: " + host, e);
}
@@ -215,7 +224,12 @@ public class CBServerConfig implements WebServerConfiguration {
this.supportedHosts.addAll(uniqueHosts);
}
public boolean isBindSessionToIp() {
@NotNull
public String getBindSessionToIp() {
return bindSessionToIp;
}
public void setBindSessionToIp(@NotNull String bindSessionToIp) {
this.bindSessionToIp = bindSessionToIp;
}
}
@@ -91,18 +91,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
Path runtimeConfigPath = getRuntimeAppConfigPath();
if (Files.exists(runtimeConfigPath)) {
log.debug("Runtime configuration [" + runtimeConfigPath.toAbsolutePath() + "]");
Map<String, Object> runtimeConfigMap = loadConfiguration(runtimeConfigPath);
Map<String, Object> serverConfigMap = JSONUtils.getObject(runtimeConfigMap, CBConstants.PARAM_SERVER_CONFIGURATION);
if (!serverConfigMap.containsKey(CBConstants.PARAM_FORCE_HTTPS)) {
CBServerConfig serverConfig = getServerConfiguration();
// enable https for legacy configurations
if (CommonUtils.isEmpty(serverConfig.getSupportedHosts())
&& CommonUtils.isNotEmpty(serverConfig.getServerURL())
&& serverConfig.getServerURL().startsWith("https://")
) {
serverConfig.setForceHttps(true);
}
}
loadConfiguration(runtimeConfigPath);
}
// Set default preferences
@@ -554,6 +543,12 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
CBConstants.PARAM_SUPPORTED_HOSTS,
serverConfig.getSupportedHosts()
);
copyConfigValue(
originServerConfig,
serverConfigProperties,
CBConstants.PARAM_BIND_SESSION_TO_IP,
serverConfig.getBindSessionToIp()
);
var productConfigProperties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
Map<String, Object> oldProductRuntimeConfig = JSONUtils.getObject(originServerConfig,
CBConstants.PARAM_PRODUCT_SETTINGS);
@@ -111,7 +111,11 @@ public class RequestHostFilter implements Filter {
return;
}
try {
String requestHost = originUri.getHost();
var requestHostBuilder = new StringBuilder(originUri.getHost());
if (originUri.getPort() > -1) {
requestHostBuilder.append(':').append(originUri.getPort());
}
String requestHost = requestHostBuilder.toString();
if (!availableHosts.contains(requestHost)) {
log.warn("Request host '" + requestHost + "' is not allowed. Redirect to default: " + availableHosts);
redirectToDefaultHost((HttpServletResponse) response, httpRequest, availableHosts);
@@ -22,6 +22,7 @@ import io.cloudbeaver.model.session.*;
import io.cloudbeaver.registry.WebHandlerRegistry;
import io.cloudbeaver.registry.WebSessionHandlerDescriptor;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.server.CBConstants;
import io.cloudbeaver.server.WebAppSessionManager;
import io.cloudbeaver.server.events.WSWebUtils;
import io.cloudbeaver.service.DBWSessionHandler;
@@ -156,8 +157,15 @@ public class CBSessionManager implements WebAppSessionManager {
}
}
validateSessionIp(request, webSession);
return webSession;
}
private void validateSessionIp(@NotNull HttpServletRequest request, WebSession webSession) {
boolean bindingEnabled = isSessionBindingEnabled(request);
String currentRemote = request.getRemoteAddr();
if (application.getServerConfiguration().isBindSessionToIp()
if (bindingEnabled
&& (CommonUtils.isEmpty(currentRemote) || !currentRemote.equals(webSession.getLastRemoteAddr()))
) {
var error = new DBWebException(
@@ -169,8 +177,11 @@ public class CBSessionManager implements WebAppSessionManager {
webSession.addSessionError(error);
closeSession(webSession.getSessionId());
}
}
return webSession;
protected boolean isSessionBindingEnabled(@NotNull HttpServletRequest request) {
String bindingState = application.getServerConfiguration().getBindSessionToIp();
return CBConstants.BIND_SESSION_ENABLE.equalsIgnoreCase(bindingState) || Boolean.parseBoolean(bindingState);
}
@NotNull
@@ -127,7 +127,8 @@ input ServerConfigInput {
sessionExpireTime: Int
forceHttps: Boolean @since(version: "25.1.3")
supportedHosts: [String!]! @since(version: "25.1.3")
supportedHosts: [String!] @since(version: "25.1.3")
bindSessionToIp: String @since(version: "25.1.4")
}
input AdminUserFilterInput {
@@ -55,6 +55,8 @@ public class AdminServerConfig {
private final List<String> supportedHosts;
private long sessionExpireTime;
@Nullable
private String bindSessionToIp;
public AdminServerConfig(Map<String, Object> params) {
this.serverName = JSONUtils.getString(params, "serverName");
@@ -115,6 +117,12 @@ public class AdminServerConfig {
} else {
this.supportedHosts = null;
}
if (params.containsKey("bindSessionToIp")) {
this.bindSessionToIp = JSONUtils.getString(params, "bindSessionToIp");
} else {
this.bindSessionToIp = CBConstants.BIND_SESSION_DISABLE;
}
}
public String getServerName() {
@@ -206,4 +214,9 @@ public class AdminServerConfig {
public Boolean getForceHttps() {
return forceHttps;
}
@Nullable
public String getBindSessionToIp() {
return bindSessionToIp;
}
}
@@ -611,6 +611,9 @@ public class WebServiceAdmin implements DBWServiceAdmin {
if (config.getSupportedHosts() != null) {
serverConfig.setSupportedHosts(config.getSupportedHosts());
}
if (config.getBindSessionToIp() != null) {
serverConfig.setBindSessionToIp(config.getBindSessionToIp());
}
}
if (CommonUtils.isEmpty(adminName)) {
@@ -28,6 +28,7 @@ export const defaultServerConfig: (productConfiguration?: Record<string, any>) =
supportsWorkspaces: false,
sessionExpireTime: 6000000,
anonymousAccessEnabled: true,
bindSessionToIp: 'disable',
adminCredentialsSaveEnabled: true,
publicCredentialsSaveEnabled: true,
secretManagerEnabled: true,
@@ -12,6 +12,7 @@ fragment ServerConfig on ServerConfig {
sessionExpireTime
anonymousAccessEnabled
bindSessionToIp
adminCredentialsSaveEnabled
publicCredentialsSaveEnabled
@@ -7,39 +7,42 @@
*/
import { observer } from 'mobx-react-lite';
import { Group, GroupTitle, Switch, useTranslate } from '@cloudbeaver/core-blocks';
import type { IServerConfig } from '../IServerConfigurationFormPartState.js';
import { Group, GroupTitle, Placeholder, Switch, useTranslate, type PlaceholderComponent } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ServerConfigurationService, type IConfigurationPlaceholderProps } from '../ServerConfigurationService.js';
interface Props {
serverConfig: IServerConfig;
}
export const ServerConfigurationSecurityForm: PlaceholderComponent<IConfigurationPlaceholderProps> = observer(
function ServerConfigurationSecurityForm({ state, configurationWizard }) {
const translate = useTranslate();
const serverConfig = state.serverConfig;
const serverConfigurationService = useService(ServerConfigurationService);
export const ServerConfigurationSecurityForm = observer<Props>(function ServerConfigurationSecurityForm({ serverConfig }) {
const translate = useTranslate();
return (
<Group form gap>
<GroupTitle>{translate('administration_configuration_wizard_configuration_security')}</GroupTitle>
<Switch
name="adminCredentialsSaveEnabled"
state={serverConfig}
description={translate('administration_configuration_wizard_configuration_security_admin_credentials_description')}
mod={['primary']}
small
autoHide
>
{translate('administration_configuration_wizard_configuration_security_admin_credentials')}
</Switch>
<Switch
name="publicCredentialsSaveEnabled"
state={serverConfig}
description={translate('administration_configuration_wizard_configuration_security_public_credentials_description')}
mod={['primary']}
disabled={!serverConfig.adminCredentialsSaveEnabled}
small
autoHide
>
{translate('administration_configuration_wizard_configuration_security_public_credentials')}
</Switch>
</Group>
);
});
return (
<Group form gap>
<GroupTitle>{translate('administration_configuration_wizard_configuration_security')}</GroupTitle>
<Switch
name="adminCredentialsSaveEnabled"
state={serverConfig}
description={translate('administration_configuration_wizard_configuration_security_admin_credentials_description')}
mod={['primary']}
small
autoHide
>
{translate('administration_configuration_wizard_configuration_security_admin_credentials')}
</Switch>
<Switch
name="publicCredentialsSaveEnabled"
state={serverConfig}
description={translate('administration_configuration_wizard_configuration_security_public_credentials_description')}
mod={['primary']}
disabled={!serverConfig.adminCredentialsSaveEnabled}
small
autoHide
>
{translate('administration_configuration_wizard_configuration_security_public_credentials')}
</Switch>
<Placeholder container={serverConfigurationService.securitySettingsContainer} configurationWizard={configurationWizard} state={state} />
</Group>
);
},
);
@@ -26,6 +26,7 @@ const ServerConfigurationFormPartStateConfigSchema = schema.object({
sessionExpireTime: schema.number().optional(),
forceHttps: schema.boolean().optional(),
supportedHosts: schema.string(),
bindSessionToIp: schema.string().optional(),
});
const ServerConfigurationFormPartStateNavigatorSchema = schema.object({
@@ -167,6 +167,7 @@ export class ServerConfigurationFormPart extends FormPart<IServerConfigurationFo
secretManagerEnabled: config?.secretManagerEnabled ?? false,
supportedHosts: config?.supportedHosts.join(SUPPORTED_HOSTS_SPLITTER) ?? '',
forceHttps: config?.forceHttps ?? true,
bindSessionToIp: config?.bindSessionToIp,
},
navigatorConfig: { ...this.state.navigatorConfig, ...defaultNavigatorSettings },
});
@@ -154,7 +154,7 @@ export const ServerConfigurationPage: AdministrationItemContentComponent = obser
<Placeholder container={serverConfigurationService.pluginsContainer} configurationWizard={configurationWizard} state={part.state} />
</Group>
<Placeholder container={serverConfigurationService.configurationContainer} configurationWizard={configurationWizard} state={part.state} />
<ServerConfigurationSecurityForm serverConfig={part.state.serverConfig} />
<ServerConfigurationSecurityForm configurationWizard={configurationWizard} state={part.state} />
<ServerConfigurationDriversForm initialServerConfig={part.initialState.serverConfig} serverConfig={part.state.serverConfig} />
</Container>
</Form>
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -25,11 +25,13 @@ export class ServerConfigurationService {
readonly configurationContainer: PlaceholderContainer<IConfigurationPlaceholderProps>;
readonly pluginsContainer: PlaceholderContainer<IConfigurationPlaceholderProps>;
readonly securitySettingsContainer: PlaceholderContainer<IConfigurationPlaceholderProps>;
constructor(private readonly serverConfigurationFormService: ServerConfigurationFormService) {
this.isDone = false;
this.configurationContainer = new PlaceholderContainer();
this.pluginsContainer = new PlaceholderContainer();
this.securitySettingsContainer = new PlaceholderContainer();
this.serverConfigurationFormService.onValidate.addHandler((data, contexts) => {
const validation = contexts.getContext(formValidationContext);
@@ -68,7 +68,6 @@ export default [
],
['administration_configuration_wizard_configuration_security_public_credentials', 'Save users credentials'],
['administration_configuration_wizard_configuration_security_public_credentials_description', 'Allows to save credentials for non-admin users'],
['administration_configuration_wizard_configuration_navigator', 'Navigator'],
['administration_configuration_wizard_configuration_navigator_hide_folders', 'Hide Folders'],
['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Hide Schemas'],
@@ -74,7 +74,6 @@ export default [
'administration_configuration_wizard_configuration_security_public_credentials_description',
"Permet d'enregistrer les identifiants pour les utilisateurs non administrateurs",
],
['administration_configuration_wizard_configuration_navigator', 'Navigateur'],
['administration_configuration_wizard_configuration_navigator_hide_folders', 'Masquer les dossiers'],
['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Masquer les schémas'],
@@ -73,7 +73,6 @@ export default [
'administration_configuration_wizard_configuration_security_public_credentials_description',
'Permetti di salvare le credenziali per gli utenti non amministratori',
],
['administration_configuration_wizard_configuration_navigator', 'Navigatore'],
['administration_configuration_wizard_configuration_navigator_hide_folders', 'Nascondi le Cartelle'],
['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Nascondi gli Schemi'],