Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public class WebConnectionInfo {
private static final String FEATURE_RESTRICT_METADATA_EDIT = "restrictMetadataEdit";

private static final String TOOL_SESSION_MANAGER = "sessionManager";

private final WebSession session;
private final DBPDataSourceContainer dataSourceContainer;
private WebServerError connectError;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ public interface WebAppConfiguration extends ServletAppConfiguration {

boolean isAdminCredentialsSaveEnabled();

default boolean isDbUserPasswordChangeEnabled() {
return false;
}

default String[] getDisabledBetaFeatures() {
return new String[0];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class CBAppConfig extends BaseWebAppConfiguration implements ServletAuthC
private boolean forwardProxy;
private boolean publicCredentialsSaveEnabled;
private boolean adminCredentialsSaveEnabled;
private boolean dbUserPasswordChangeEnabled;
private boolean linkExternalCredentialsWithUser;

private boolean redirectOnFederatedAuth;
Expand Down Expand Up @@ -80,6 +81,7 @@ public CBAppConfig() {
this.supportsCustomConnections = true;
this.publicCredentialsSaveEnabled = true;
this.adminCredentialsSaveEnabled = true;
this.dbUserPasswordChangeEnabled = false;
this.redirectOnFederatedAuth = false;
this.enabledDrivers = new String[0];
this.disabledDrivers = new String[0];
Expand All @@ -104,6 +106,7 @@ public CBAppConfig(CBAppConfig src) {
this.supportsCustomConnections = src.supportsCustomConnections;
this.publicCredentialsSaveEnabled = src.publicCredentialsSaveEnabled;
this.adminCredentialsSaveEnabled = src.adminCredentialsSaveEnabled;
this.dbUserPasswordChangeEnabled = src.dbUserPasswordChangeEnabled;
this.redirectOnFederatedAuth = src.redirectOnFederatedAuth;
this.enabledDrivers = src.enabledDrivers;
this.disabledDrivers = src.disabledDrivers;
Expand Down Expand Up @@ -154,6 +157,14 @@ public void setPublicCredentialsSaveEnabled(boolean publicCredentialsSaveEnabled
this.publicCredentialsSaveEnabled = publicCredentialsSaveEnabled;
}

public boolean isDbUserPasswordChangeEnabled() {
return dbUserPasswordChangeEnabled;
}

public void setDbUserPasswordChangeEnabled(boolean dbUserPasswordChangeEnabled) {
this.dbUserPasswordChangeEnabled = dbUserPasswordChangeEnabled;
}

public boolean isAdminCredentialsSaveEnabled() {
return adminCredentialsSaveEnabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class AdminServerConfig {
private final boolean customConnectionsEnabled;
private final boolean publicCredentialsSaveEnabled;
private final boolean adminCredentialsSaveEnabled;
private final boolean dbUserPasswordChangeEnabled;
private final List<String> enabledFeatures;
private final List<String> enabledAuthProviders;
private final String[] enabledDrivers;
Expand Down Expand Up @@ -75,6 +76,11 @@ public AdminServerConfig(@NotNull Map<String, Object> params) {
"adminCredentialsSaveEnabled",
appConfig.isAdminCredentialsSaveEnabled()
);
this.dbUserPasswordChangeEnabled = JSONUtils.getBoolean(
params,
"dbUserPasswordChangeEnabled",
appConfig.isDbUserPasswordChangeEnabled()
);
this.resourceManagerEnabled = JSONUtils.getBoolean(params, "resourceManagerEnabled", appConfig.isResourceManagerEnabled());
this.secretManagerEnabled = JSONUtils.getBoolean(params, "secretManagerEnabled", appConfig.isSecretManagerEnabled());

Expand Down Expand Up @@ -163,6 +169,10 @@ public boolean isAdminCredentialsSaveEnabled() {
return adminCredentialsSaveEnabled;
}

public boolean isDbUserPasswordChangeEnabled() {
return dbUserPasswordChangeEnabled;
}

public long getSessionExpireTime() {
return sessionExpireTime;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,11 @@ protected Map<String, Object> collectConfigurationProperties(
appConfigProperties,
"adminCredentialsSaveEnabled",
appConfig.isAdminCredentialsSaveEnabled());
copyConfigValue(
oldAppConfig,
appConfigProperties,
"dbUserPasswordChangeEnabled",
appConfig.isDbUserPasswordChangeEnabled());
copyConfigValue(
oldAppConfig, appConfigProperties, "enableReverseProxyAuth", appConfig.isEnabledReverseProxyAuth());
copyConfigValue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ protected void populateConfigurations(@NotNull I input, @NotNull C serverConfig,
appConfig.setSupportsCustomConnections(input.isCustomConnectionsEnabled());
appConfig.setPublicCredentialsSaveEnabled(input.isPublicCredentialsSaveEnabled());
appConfig.setAdminCredentialsSaveEnabled(input.isAdminCredentialsSaveEnabled());
appConfig.setDbUserPasswordChangeEnabled(input.isDbUserPasswordChangeEnabled());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we need to add the configuration parameter to config/template/cloudbeaver-base.conf

updateDisabledFeaturesConfig(appConfig, input.getEnabledFeatures());
// custom logic for enabling embedded drivers
updateDisabledDriversConfig(appConfig, input.getDisabledDrivers());
Expand Down
3 changes: 3 additions & 0 deletions server/bundles/io.cloudbeaver.server/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@
<eventHandler class="io.cloudbeaver.server.events.WSObjectSettingsEventHandler">
<topic id="cb_object_settings"/>
</eventHandler>
<eventHandler class="io.cloudbeaver.server.events.WSSecurityAuditEventHandler">
<topic id="cb_security_audit"/>
</eventHandler>
</extension>

<extension point="org.jkiss.dbeaver.settings">
Expand Down
15 changes: 15 additions & 0 deletions server/bundles/io.cloudbeaver.server/schema/service.core.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ type ServerConfig {
"Defines is it is possible to save global database credentials"
adminCredentialsSaveEnabled: Boolean!

"Defines if the change-DB-password mutation is enabled server-wide"
dbUserPasswordChangeEnabled: Boolean!

"Defines if the server requires a license"
licenseRequired: Boolean!
"Defines if the server license is valid"
Expand Down Expand Up @@ -902,6 +905,18 @@ extend type Mutation {
"Test connection configuration. Returns remote server version"
testConnection( config: ConnectionConfig!, projectId: ID): ConnectionInfo!

"""
Change the DB user password backing this connection.
Requires PERMISSION_PROJECT_DATASOURCES_EDIT on the connection's project.
DBWebException surfaces the driver's exception verbatim on DB rejection.
"""
changeConnectionUserPassword(
projectId: ID,
connectionId: ID!,
oldPassword: String!,
newPassword: String!
): Boolean!

"Test network handler connectivity"
testNetworkHandler(projectId: ID, connectionId: ID, config: NetworkHandlerConfigInput! ): NetworkEndpointInfo!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ public boolean isAdminCredentialsSaveEnabled() {
return application.getAppConfiguration().isAdminCredentialsSaveEnabled();
}

@Property
public boolean isDbUserPasswordChangeEnabled() {
return application.getAppConfiguration().isDbUserPasswordChangeEnabled();
}

@Property
public boolean isLicenseRequired() {
return application.isLicenseRequired();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2026 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.server.events;

import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.websocket.event.WSAbstractEvent;

public class WSSecurityAuditEvent extends WSAbstractEvent {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need that class? I think it would be better just write logs as usual by using Log.getLog(Class.class)

public static final String TOPIC = "cb_security_audit";
public static final String ID = "cb_security_audit_updated";

public enum Kind {
/** Fires before invoking the DBeaver core password-change handler. */
ATTEMPTED,
/** Fires after handler success and credential-store persistence. */
SUCCEEDED,
/** Fires on handler exception or credential-store persistence failure. */
FAILED,
/** Fires on any of the pre-invocation gate rejections. */
GATE_REJECTED
}

@Nullable
private final String projectId;
@Nullable
private final String connectionId;
@Nullable
private final String driverId;
@NotNull
private final Kind kind;
@Nullable
private final String reasonCode;
@Nullable
private final String errorClass;

public WSSecurityAuditEvent(
@Nullable String sessionId,
@Nullable String userId,
@Nullable String projectId,
@Nullable String connectionId,
@Nullable String driverId,
@NotNull Kind kind,
@Nullable String reasonCode,
@Nullable String errorClass
) {
super(ID, TOPIC, sessionId, userId);
this.projectId = projectId;
this.connectionId = connectionId;
this.driverId = driverId;
this.kind = kind;
this.reasonCode = reasonCode;
this.errorClass = errorClass;
}

@Nullable
public String getProjectId() {
return projectId;
}

@Nullable
public String getConnectionId() {
return connectionId;
}

@Nullable
public String getDriverId() {
return driverId;
}

@NotNull
public Kind getKind() {
return kind;
}

@Nullable
public String getReasonCode() {
return reasonCode;
}

@Nullable
public String getErrorClass() {
return errorClass;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2026 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.server.events;

import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.websocket.WSEventHandler;

public class WSSecurityAuditEventHandler implements WSEventHandler<WSSecurityAuditEvent> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think that we need it.

private static final Log auditLog = Log.getLog(WSSecurityAuditEventHandler.class);

@Override
public void handleEvent(@NotNull WSSecurityAuditEvent event) {
auditLog.info(String.format(
"topic=%s id=%s kind=%s reasonCode=%s userId=%s sessionId=%s "
+ "projectId=%s connectionId=%s driverId=%s errorClass=%s timestamp=%d",
event.getTopicId(),
event.getId(),
event.getKind(),
event.getReasonCode(),
event.getUserId(),
event.getSessionId(),
event.getProjectId(),
event.getConnectionId(),
event.getDriverId(),
event.getErrorClass(),
event.getTimestamp()
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ WebConnectionInfo testConnection(
@NotNull Map<String, Object> connectionConfig
) throws DBWebException;

@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it does contradict the issue. I started implementing the shape and realized that it doesn't make sense to have a per-team pw-reset block if the user can just proceed to change their password through an SQL prompt. Reasons to block PW-Resets are for shared-connections which use service accounts (when the user doesn't have edit permission on the connection object), or when an external authentication mechanism like RADIUS or OIDC are in-use.

If the user has permission to run statements, they usually have permission to change their passwords on the DB technologies known to me.

The only narrow use-case in which this change would expose an undesired credential change is, if the database was set up with PERMISSION_PROJECT_DATASOURCES_EDIT or an analogue setting, while having no SQL execution rights. This would allow the user to change the password through the GUI while they couldn't through an SQL worksheet. However, this is extremely specific and the only rationale I could think of to have per-team PW-update settings.

I did generally not treat the issue as a hard policy or contract and deviated for these reasons. But I can change it, if that's desired.

boolean changeConnectionUserPassword(
@NotNull WebSession webSession,
@Nullable @WebObjectId String projectId,
@NotNull String connectionId,
@WebParameterSecure @NotNull String oldPassword,
@WebParameterSecure @NotNull String newPassword
) throws DBWebException;

@WebProjectAction(requireProjectPermissions = {RMConstants.PERMISSION_PROJECT_DATASOURCES_EDIT})
WebNetworkEndpointInfo testNetworkHandler(
@NotNull WebSession webSession,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,13 @@ public void bindWiring(DBWBindingContext model) throws DBWebException {
.dataFetcher("testConnection", env -> getService(env).testConnection(
getWebSession(env), getProjectReference(env), getArgumentVal(env, "config")
))
.dataFetcher("changeConnectionUserPassword", env -> getService(env).changeConnectionUserPassword(
getWebSession(env),
getProjectReference(env),
getArgumentVal(env, "connectionId"),
getArgumentVal(env, "oldPassword"),
getArgumentVal(env, "newPassword")
))
.dataFetcher("testNetworkHandler", env -> getService(env).testNetworkHandler(
getWebSession(env),
getProjectReference(env),
Expand Down
Loading