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 4fe539750e3..40cf606df08 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 @@ -72,6 +72,11 @@ public class WebConnectionInfo { private static final String FEATURE_PROVIDED = "provided"; private static final String FEATURE_MANAGEABLE = "manageable"; + private static final String FEATURE_RESTRICT_DATA_EDIT = "restrictDataEdit"; + private static final String FEATURE_RESTRICT_SCRIPT_EXECUTE = "restrictScriptExecute"; + private static final String FEATURE_RESTRICT_DATA_IMPORT = "restrictDataImport"; + private static final String FEATURE_RESTRICT_METADATA_EDIT = "restrictMetadataEdit"; + private static final String TOOL_SESSION_MANAGER = "sessionManager"; private final WebSession session; @@ -275,6 +280,18 @@ public String[] getFeatures() { if (dataSourceContainer.isConnectionReadOnly()) { features.add(FEATURE_READ_ONLY); } + if (!dataSourceContainer.hasModifyPermission(DBPDataSourcePermission.PERMISSION_EDIT_DATA)) { + features.add(FEATURE_RESTRICT_DATA_EDIT); + } + if (!dataSourceContainer.hasModifyPermission(DBPDataSourcePermission.PERMISSION_EXECUTE_SCRIPTS)) { + features.add(FEATURE_RESTRICT_SCRIPT_EXECUTE); + } + if (!dataSourceContainer.hasModifyPermission(DBPDataSourcePermission.PERMISSION_IMPORT_DATA)) { + features.add(FEATURE_RESTRICT_DATA_IMPORT); + } + if (!dataSourceContainer.hasModifyPermission(DBPDataSourcePermission.PERMISSION_EDIT_METADATA)) { + features.add(FEATURE_RESTRICT_METADATA_EDIT); + } if (dataSourceContainer.isProvided()) { features.add(FEATURE_PROVIDED); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index a35c46d4e30..7783053edcf 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -461,6 +461,7 @@ public String renameNode( return node.getNodeUri(); } if (node instanceof DBNDatabaseNode dbNode) { + checkMetadataEditPermission(dbNode); return renameDatabaseObject( session, dbNode, @@ -545,8 +546,9 @@ public int deleteNodes( throw new DBWebException("Navigator node '" + path + "' not found"); } checkProjectEditAccess(node, session); - if (node instanceof DBNDatabaseNode) { - DBSObject object = ((DBNDatabaseNode) node).getObject(); + if (node instanceof DBNDatabaseNode dbnDatabaseNode) { + checkMetadataEditPermission(dbnDatabaseNode); + DBSObject object = dbnDatabaseNode.getObject(); DBEObjectMaker objectDeleter = DBWorkbench.getPlatform().getEditorsRegistry().getObjectManager( object.getClass(), DBEObjectMaker.class); if (objectDeleter == null || !objectDeleter.canDeleteObject(object)) { @@ -602,6 +604,12 @@ public int deleteNodes( } } + private void checkMetadataEditPermission(@NotNull DBNDatabaseNode node) throws DBException { + if (!node.getDataSourceContainer().hasModifyPermission(DBPDataSourcePermission.PERMISSION_EDIT_METADATA)) { + throw new DBWebException("Structure edit is restricted for this connection"); + } + } + private void checkProjectEditAccess(@NotNull DBNNode node, @NotNull WebSession session) throws DBException { var project = node.getOwnerProject(); if (!(project instanceof BaseWebProjectImpl bwp) || !hasNodeEditPermission(session, node, bwp.getRMProject())) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 1c0372c6a5f..4f6d6ad50d1 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -31,10 +31,7 @@ import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBPDataKind; -import org.jkiss.dbeaver.model.DBPDataSource; -import org.jkiss.dbeaver.model.DBPDataSourceContainer; -import org.jkiss.dbeaver.model.DBUtils; +import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; @@ -509,6 +506,7 @@ private WebSQLExecuteInfo updateResultsDataBatch( @Nullable List addedRows, @Nullable WebDataFormat dataFormat ) throws DBException { + checkDataEditPermission(contextInfo); WebSQLExecuteInfo[] result = new WebSQLExecuteInfo[1]; DBExecUtils.tryExecuteRecover( @@ -521,6 +519,13 @@ private WebSQLExecuteInfo updateResultsDataBatch( return result[0]; } + private void checkDataEditPermission(@NotNull WebSQLContextInfo contextInfo) throws DBWebException { + if (!contextInfo.getProcessor().getConnection().getDataSourceContainer() + .hasModifyPermission(DBPDataSourcePermission.PERMISSION_EDIT_DATA)) { + throw new DBWebException("Data edit is restricted for this connection"); + } + } + @FunctionalInterface private interface ThrowableFunction { R apply(T obj) throws DBException; @@ -573,6 +578,7 @@ private String readValue( @Override public String updateResultsDataBatchScript(@NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @Nullable List updatedRows, @Nullable List deletedRows, @Nullable List addedRows, WebDataFormat dataFormat) throws DBWebException { + checkDataEditPermission(contextInfo); try { return contextInfo.getProcessor().generateResultsDataUpdateScript( contextInfo.getProcessor().getWebSession().getProgressMonitor(), @@ -599,6 +605,10 @@ public WebAsyncTaskInfo asyncExecuteQuery( if (DBWorkbench.isDistributed() && !webSession.hasPermission(DBWConstants.PERMISSION_SQL_EXECUTE_QUERY)) { throw new DBWebException("Permission denied"); } + if (!contextInfo.getProcessor().getConnection().getDataSourceContainer() + .hasModifyPermission(DBPDataSourcePermission.PERMISSION_EXECUTE_SCRIPTS)) { + throw new DBWebException("Script execution is restricted for this connection"); + } return WebSQLUtils.createAsyncTaskExecuteSqlQuery( webSession, contextInfo, diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java index 86a93c4896c..77acb2cbc6a 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java +++ b/server/bundles/io.cloudbeaver.service.data.transfer/src/io/cloudbeaver/service/data/transfer/impl/WebServiceDataTransfer.java @@ -32,6 +32,7 @@ import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourcePermission; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -245,6 +246,10 @@ public WebAsyncTaskInfo asyncImportDataContainer( if (!validateImportPermission(webSession)) { throw new DBWebException("Permission denied. Data import is not allowed for this user"); } + if (!sqlContext.getProcessor().getConnection().getDataSourceContainer() + .hasModifyPermission(DBPDataSourcePermission.PERMISSION_IMPORT_DATA)) { + throw new DBWebException("Data import is restricted for this connection"); + } DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(parameters.getProcessorId()); if (processor == null) { throw new DBWebException("Wrong data processor '" + parameters.getProcessorId() + "'"); diff --git a/webapp/packages/core-connections/src/EConnectionFeature.ts b/webapp/packages/core-connections/src/EConnectionFeature.ts index 757e49eebe5..106afc2852a 100644 --- a/webapp/packages/core-connections/src/EConnectionFeature.ts +++ b/webapp/packages/core-connections/src/EConnectionFeature.ts @@ -1,6 +1,6 @@ /* * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others + * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. @@ -13,4 +13,9 @@ export enum EConnectionFeature { readOnly = 'readOnly', provided = 'provided', manageable = 'manageable', + + restrictDataEdit = 'restrictDataEdit', + restrictScriptExecute = 'restrictScriptExecute', + restrictDataImport = 'restrictDataImport', + restrictMetadataEdit = 'restrictMetadataEdit', } diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageActionsService.ts b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageActionsService.ts index 1dcd5189a95..5cbfa20ccc9 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageActionsService.ts +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/AIChatMessageActionsService.ts @@ -16,9 +16,9 @@ import { ESqlDataSourceFeatures, LocalStorageSqlDataSource, SqlDataSourceService, + SqlEditorPermissionService, SqlEditorService, type ILocalStorageSqlDataSourceState, - SqlEditorSettingsService, type ISqlEditorTabState, } from '@cloudbeaver/plugin-sql-editor'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -41,12 +41,9 @@ const AI_CHAT_ID_METADATA_KEY = 'aiChatId'; SqlDataSourceService, LocalizationService, CommonDialogService, - SqlEditorSettingsService, + SqlEditorPermissionService, ]) export class AIChatMessageActionsService { - get isAllowed(): boolean { - return this.sqlEditorSettingsService.scriptExecutionEnabled; - } get isDisabled(): boolean { return !this.aiChatContextService.currentContext; } @@ -61,13 +58,21 @@ export class AIChatMessageActionsService { private readonly sqlDataSourceService: SqlDataSourceService, private readonly localizationService: LocalizationService, private readonly commonDialogService: CommonDialogService, - private readonly sqlEditorSettingsService: SqlEditorSettingsService, + private readonly sqlEditorPermissionService: SqlEditorPermissionService, ) { makeObservable(this, { isDisabled: computed, }); } + isAllowed(connectionKey: IConnectionInfoParams | null): boolean { + if (!connectionKey) { + return false; + } + + return this.sqlEditorPermissionService.isScriptExecutionEnabled(connectionKey); + } + async executeQuery(conversationId: string | undefined | null, connectionKey: IConnectionInfoParams | null, query: string): Promise { const currentTab = this.navigationTabsService.currentTab; diff --git a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/CodeFormatter.tsx b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/CodeFormatter.tsx index 433325a875c..46fc209fa17 100644 --- a/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/CodeFormatter.tsx +++ b/webapp/packages/plugin-ai-chat/src/AIChat/AIChatMessage/CodeFormatter.tsx @@ -62,7 +62,7 @@ export const CodeFormatter = observer(function CodeFormatter({ code, conv