Merge branch 'devel' into dbeaver/pro#10157-fix-readable-error-for-cloud

This commit is contained in:
Daria Marutkina
2026-08-17 14:51:59 +02:00
committed by GitHub
27 changed files with 210 additions and 77 deletions
@@ -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 class WebConnectionInfo {
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);
}
@@ -66,7 +66,7 @@
<driver id="generic:trino_jdbc"/>
<driver id="duckdb:duckdb_jdbc"/>
<driver id="generic:kyuubi_hive"/>
<driver id="databend:databend"/>
<driver id="databend:databend_jdbc"/>
</extension>
@@ -461,6 +461,7 @@ public class WebServiceNavigator implements DBWServiceNavigator {
return node.getNodeUri();
}
if (node instanceof DBNDatabaseNode dbNode) {
checkMetadataEditPermission(dbNode);
return renameDatabaseObject(
session,
dbNode,
@@ -545,8 +546,9 @@ public class WebServiceNavigator implements DBWServiceNavigator {
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 class WebServiceNavigator implements DBWServiceNavigator {
}
}
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())) {
@@ -31,10 +31,7 @@ import org.jkiss.code.NotNull;
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 @@ public class WebServiceSQL implements DBWServiceSQL {
@Nullable List<WebSQLResultsRow> addedRows,
@Nullable WebDataFormat dataFormat
) throws DBException {
checkDataEditPermission(contextInfo);
WebSQLExecuteInfo[] result = new WebSQLExecuteInfo[1];
DBExecUtils.tryExecuteRecover(
@@ -521,6 +519,13 @@ public class WebServiceSQL implements DBWServiceSQL {
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<T, R> {
R apply(T obj) throws DBException;
@@ -573,6 +578,7 @@ public class WebServiceSQL implements DBWServiceSQL {
@Override
public String updateResultsDataBatchScript(@NotNull WebSQLContextInfo contextInfo, @NotNull String resultsId, @Nullable List<WebSQLResultsRow> updatedRows, @Nullable List<WebSQLResultsRow> deletedRows, @Nullable List<WebSQLResultsRow> addedRows, WebDataFormat dataFormat) throws DBWebException {
checkDataEditPermission(contextInfo);
try {
return contextInfo.getProcessor().generateResultsDataUpdateScript(
contextInfo.getProcessor().getWebSession().getProgressMonitor(),
@@ -599,6 +605,10 @@ public class WebServiceSQL implements DBWServiceSQL {
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,
@@ -32,6 +32,7 @@ import org.jkiss.code.NotNull;
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 class WebServiceDataTransfer implements DBWServiceDataTransfer {
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() + "'");
@@ -98,6 +98,8 @@ export interface ISelectFieldProps<T, ItemType = ISelectItem<T>> {
portal?: boolean;
overflowPadding?: number;
autoFocusItemsOnShow?: boolean;
'aria-labelledby'?: string;
@@ -144,6 +146,7 @@ export function SelectField<T, ItemType extends {} = ISelectItem<T>>({
required,
className,
portal = false,
overflowPadding,
selectedRender,
arrowIcon,
store,
@@ -229,7 +232,7 @@ export function SelectField<T, ItemType extends {} = ISelectItem<T>>({
</Select>
{description && <span className="dbv-kit-select__description">{description}</span>}
<SelectPopover autoFocusOnShow={autoFocusItemsOnShow} portal={portal} gutter={4} unmountOnHide>
<SelectPopover autoFocusOnShow={autoFocusItemsOnShow} portal={portal} overflowPadding={overflowPadding} gutter={4} unmountOnHide>
{headerItems && headerItems.length > 0 && (
<SelectGroup className="dbv-kit-select__popover-header">{headerItems.map(renderSelectItem)}</SelectGroup>
)}
@@ -41,6 +41,7 @@ export type SelectBaseProps<TKey, TValue> = Omit<
inline?: boolean;
children?: string;
portal?: boolean;
overflowPadding?: number;
headerItems?: TValue[];
footerItems?: TValue[];
};
@@ -71,6 +72,7 @@ export const Select: ISelectType = observer(function Select({
state,
items,
portal = false,
overflowPadding,
loading,
children,
title,
@@ -158,7 +160,9 @@ export const Select: ISelectType = observer(function Select({
return (
<div className="select__item">
{renderIcon(item)}
<span className="tw:truncate tw:min-w-0" title={titleSelector?.(item) ?? valueSelector(item)}>{valueSelector(item)}</span>
<span className="tw:truncate tw:min-w-0" title={titleSelector?.(item) ?? valueSelector(item)}>
{valueSelector(item)}
</span>
</div>
);
}
@@ -189,6 +193,7 @@ export const Select: ISelectType = observer(function Select({
<SelectField
{...rest}
portal={portal}
overflowPadding={overflowPadding}
items={items}
value={value}
id={inputId}
@@ -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',
}
@@ -94,8 +94,8 @@ export const AIAdministrationPage = observer<{
</ToolsAction>
</ToolsPanel>
</Group>
<Container gap overflow>
<Group gap keepSize medium>
<Group gap keepSize medium>
<Container gap vertical>
<GroupTitle>{translate('ai_administration_settings')}</GroupTitle>
<Select
items={profiles}
@@ -122,8 +122,8 @@ export const AIAdministrationPage = observer<{
>
{translate('plugin_ai_administration_language_label')}
</Combobox>
</Group>
</Container>
</Container>
</Group>
</ColoredContainer>
</Form>
);
@@ -86,26 +86,28 @@ export const AIProfileOptions: TabContainerPanelComponent<IAIProfileFormProps> =
return (
<ColoredContainer wrap overflow parent gap>
<Container medium gap>
<Group form gap>
<InputField ref={nameRef} name="name" state={part.state} disabled={formState.isDisabled} small required>
{translate('plugin_ai_administration_profile_form_field_name')}
</InputField>
<Select
items={enginesLoader.data}
valueSelector={value => value.name}
keySelector={value => value.id}
value={part.state.engineId}
disabled={formState.isDisabled || isEditMode}
iconSelector={value => value.icon}
small
required
onSelect={value => value && part.changeEngine(value)}
>
{translate('plugin_ai_administration_profile_form_field_engine')}
</Select>
<Group gap>
<Container vertical gap>
<InputField ref={nameRef} name="name" state={part.state} disabled={formState.isDisabled} small required>
{translate('plugin_ai_administration_profile_form_field_name')}
</InputField>
<Select
items={enginesLoader.data}
valueSelector={value => value.name}
keySelector={value => value.id}
value={part.state.engineId}
disabled={formState.isDisabled || isEditMode}
iconSelector={value => value.icon}
small
required
onSelect={value => value && part.changeEngine(value)}
>
{translate('plugin_ai_administration_profile_form_field_engine')}
</Select>
</Container>
</Group>
{!!part.state.engineId && (
<Group gap keepSize medium>
<Group gap medium>
<GroupTitle>{translate('ai_administration_language_model_settings')}</GroupTitle>
<Container vertical gap>
{hasModels && (
@@ -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<void> {
const currentTab = this.navigationTabsService.currentTab;
@@ -62,7 +62,7 @@ export const CodeFormatter = observer<Props>(function CodeFormatter({ code, conv
<ActionIconButton
name="/icons/sql_exec.svg"
title={translate('plugin_ai_chat_query_execute')}
hidden={!aiChatMessageActionsService.isAllowed}
hidden={!aiChatMessageActionsService.isAllowed(connectionKey)}
disabled={disabled}
img
onClick={execute}
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Loader } from '@cloudbeaver/core-blocks';
import { Link, Loader } from '@cloudbeaver/core-blocks';
import { CodeFormatter } from './CodeFormatter.js';
@@ -24,6 +24,7 @@ export const MarkdownFormatter = observer<Props>(function MarkdownFormatter({ co
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
a: ({ node, ...props }) => <Link target="_blank" rel="noopener noreferrer" indicator inline {...props} />,
h1: ({ node, ...props }) => <h1 className="tw:text-xl! tw:font-semibold" {...props} />,
h2: ({ node, ...props }) => <h2 className="tw:text-lg! tw:font-semibold" {...props} />,
h3: ({ node, ...props }) => <h3 className="tw:text-lg! tw:font-semibold" {...props} />,
@@ -37,9 +37,9 @@ export const UserFormInfo: TabContainerPanelComponent<UserFormProps> = observer(
const tabState = useTabState<UserFormInfoPart>();
const userFormInfoPartService = useService(UserFormInfoPartService);
const administrationUsersManagementService = useService(AdministrationUsersManagementService);
const userId = tabState.state.userId ?? formState.state.userId;
const userId = formState.state.userId;
const user = useResource(UserFormInfo, UsersResource, userId, {
active: formState.mode === 'edit',
active: !!userId,
});
useAutoLoad(UserFormInfo, [tabState, ...administrationUsersManagementService.loaders], tab.selected);
@@ -5,10 +5,10 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createConnectionParam } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { ACTION_IMPORT, ActionService, menuExtractItems, MenuService } from '@cloudbeaver/core-view';
import { ConnectionInfoResource, createConnectionParam, EConnectionFeature } from '@cloudbeaver/core-connections';
import {
DATA_CONTEXT_DV_DDM,
DATA_CONTEXT_DV_DDM_RESULT_INDEX,
@@ -17,18 +17,21 @@ import {
DatabaseDataFeature,
DataViewerPresentationType,
isResultSetDataModel,
ResultSetDataSource,
type IDatabaseDataModel,
} from '@cloudbeaver/plugin-data-viewer';
import { DataImportDialogLazy } from './DataImportDialog/DataImportDialogLazy.js';
import { DataImportService } from './DataImportService.js';
@injectable(() => [MenuService, ActionService, CommonDialogService, DataImportService])
@injectable(() => [MenuService, ActionService, CommonDialogService, DataImportService, ConnectionInfoResource])
export class DataImportBootstrap extends Bootstrap {
constructor(
private readonly menuService: MenuService,
private readonly actionService: ActionService,
private readonly commonDialogService: CommonDialogService,
private readonly dataImportService: DataImportService,
private readonly connectionInfoResource: ConnectionInfoResource,
) {
super();
}
@@ -101,9 +104,21 @@ export class DataImportBootstrap extends Bootstrap {
menus: [DATA_VIEWER_DATA_MODEL_ACTIONS_MENU],
contexts: [DATA_CONTEXT_DV_DDM, DATA_CONTEXT_DV_DDM_RESULT_INDEX],
isApplicable: context => {
const model = context.get(DATA_CONTEXT_DV_DDM)!;
const model = context.get(DATA_CONTEXT_DV_DDM)! as unknown as IDatabaseDataModel<ResultSetDataSource>;
const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION);
const resultIndex = context.get(DATA_CONTEXT_DV_DDM_RESULT_INDEX)!;
const executionContext = model.source.executionContext?.context;
if (executionContext) {
const connectionKey = createConnectionParam(executionContext.projectId, executionContext.connectionId);
const connection = this.connectionInfoResource.get(connectionKey);
if (connection?.features.includes(EConnectionFeature.restrictDataImport)) {
return false;
}
}
const allowedFeatures = [DatabaseDataFeature.DataEditor];
return (
@@ -93,6 +93,7 @@ export const DataViewerReferencesPresentation: DataPresentationComponent = obser
keySelector={association => association.id}
titleSelector={getValueSelector}
valueSelector={getValueSelector}
overflowPadding={0}
iconSelector={association => (
<IconOrImage
className={clsx(association.reference && 'tw:rotate-180')}
@@ -1,11 +1,11 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 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.
*/
import type { Connection } from '@cloudbeaver/core-connections';
import { EConnectionFeature, type Connection } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { EAdminPermission, SessionPermissionsResource } from '@cloudbeaver/core-root';
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
@@ -37,7 +37,7 @@ export class DataViewerService {
}
isDataEditable(connection: Connection) {
if (connection.readOnly) {
if (connection.readOnly || connection.features.includes(EConnectionFeature.restrictDataEdit)) {
return false;
}
@@ -26,7 +26,7 @@ import {
nodeDeleteContext,
NodeManagerUtils,
} from '@cloudbeaver/core-navigation-tree';
import { isConnectionNode } from '@cloudbeaver/core-connections';
import { ConnectionInfoResource, DATA_CONTEXT_CONNECTION, EConnectionFeature, isConnectionNode } from '@cloudbeaver/core-connections';
import { ResourceKeyUtils } from '@cloudbeaver/core-resource';
import {
ACTION_DELETE,
@@ -59,6 +59,7 @@ export interface INodeMenuData {
LocalizationService,
NavNodeInfoResource,
NavTreeSettingsService,
ConnectionInfoResource,
])
export class NavNodeContextMenuService extends Bootstrap {
constructor(
@@ -71,6 +72,7 @@ export class NavNodeContextMenuService extends Bootstrap {
private readonly localizationService: LocalizationService,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly navTreeSettingsService: NavTreeSettingsService,
private readonly connectionInfoResource: ConnectionInfoResource,
) {
super();
}
@@ -116,6 +118,13 @@ export class NavNodeContextMenuService extends Bootstrap {
contexts: [DATA_CONTEXT_NAV_NODE],
isActionApplicable: (context, action) => {
const node = context.get(DATA_CONTEXT_NAV_NODE)!;
const connectionKey = context.get(DATA_CONTEXT_CONNECTION)!;
const connection = this.connectionInfoResource.get(connectionKey);
if (connection?.features.includes(EConnectionFeature.restrictMetadataEdit)) {
return false;
}
if (NodeManagerUtils.isDatabaseObject(node.uri) || isConnectionFolder(node)) {
if (action === ACTION_RENAME) {
@@ -54,7 +54,7 @@ export class SqlExecutionPlanMenuBootstrap extends Bootstrap {
isActionApplicable: contexts => {
const data = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA);
if (!data || !data.isExecutionAllowed) {
if (!data || !data.isExecutionAllowed()) {
return false;
}
@@ -86,7 +86,7 @@ export class SqlExecutionPlanMenuBootstrap extends Bootstrap {
const isQuery = data.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
if (!isQuery || !data.isExecutionAllowed || !data.dialect?.supportsExplainExecutionPlan) {
if (!isQuery || !data.isExecutionAllowed() || !data.dialect?.supportsExplainExecutionPlan) {
return;
}
@@ -244,7 +244,7 @@ export class MenuBootstrap extends Bootstrap {
return false;
}
if (!sqlEditorData.isExecutionAllowed && EXECUTIONS_ACTIONS.includes(action)) {
if (!sqlEditorData.isExecutionAllowed() && EXECUTIONS_ACTIONS.includes(action)) {
return false;
}
@@ -314,7 +314,7 @@ export class MenuBootstrap extends Bootstrap {
contexts: [DATA_CONTEXT_SQL_EDITOR_DATA],
isBindingApplicable: (contexts, action) => {
const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA);
return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.isExecutionAllowed === true;
return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.isExecutionAllowed() === true;
},
handler: this.sqlEditorActionHandler.bind(this),
});
@@ -30,13 +30,13 @@ export interface ISQLEditorData {
readonly isIncomingChanges: boolean;
readonly value: string;
readonly incomingValue?: string;
readonly isExecutionAllowed: boolean;
readonly onExecute: ISyncExecutor<boolean>;
readonly onSegmentExecute: ISyncExecutor<ISegmentExecutionData>;
readonly onFormat: ISyncExecutor<[ISQLScriptSegment, string]>;
/** displays if last getHintProposals call ended with limit */
readonly hintsLimitIsMet: boolean;
isExecutionAllowed(): boolean;
updateParserScriptsDebounced(): Promise<void>;
setScript(query: string, source?: string, cursor?: ISqlEditorCursor): void;
setCursor(begin: number, end?: number): void;
@@ -33,6 +33,7 @@ import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService.js';
import type { ISQLEditorData } from './ISQLEditorData.js';
import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js';
import { SqlEditorModelService } from '../SqlEditorModel/SqlEditorModelService.js';
import { SqlEditorPermissionService } from '../SqlEditorPermissionService.js';
interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly sqlDialectInfoService: SqlDialectInfoService;
@@ -44,6 +45,7 @@ interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly commonDialogService: CommonDialogService;
readonly sqlResultTabsService: SqlResultTabsService;
readonly connectionInfoResource: ConnectionInfoResource;
readonly sqlEditorPermissionService: SqlEditorPermissionService;
readonly getLastAutocomplete: LastPromiseGetter<SqlCompletionProposal[]>;
readonly parseScript: LastPromiseGetter<SqlScriptInfoFragment>;
@@ -68,6 +70,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
const sqlEditorSettingsService = useService(SqlEditorSettingsService);
const sqlEditorModelService = useService(SqlEditorModelService);
const connectionInfoResource = useService(ConnectionInfoResource);
const sqlEditorPermissionService = useService(SqlEditorPermissionService);
const model = sqlEditorModelService.getOrCreate(state);
@@ -119,10 +122,6 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
return this.model.dataSource?.incomingScript;
},
get isExecutionAllowed(): boolean {
return !!this.model.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) && this.sqlEditorSettingsService.scriptExecutionEnabled;
},
onExecute: new SyncExecutor(),
onSegmentExecute: new SyncExecutor(),
@@ -131,6 +130,19 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
reactionDisposer: null,
hintsLimitIsMet: false,
isExecutionAllowed(): boolean {
const executionContext = this.model.dataSource?.executionContext;
if (!executionContext) {
return false;
}
const key = createConnectionParam(executionContext.projectId, executionContext.connectionId);
return (
!!this.model.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) && this.sqlEditorPermissionService.isScriptExecutionEnabled(key)
);
},
setCursor(begin: number, end = begin): void {
this.model.dataSource?.setCursor(begin, end);
},
@@ -194,7 +206,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
async executeQuery(inNewTab = false): Promise<void> {
const isQuery = this.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
if (!isQuery || !this.isExecutionAllowed) {
if (!isQuery || !this.isExecutionAllowed()) {
return;
}
@@ -211,7 +223,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
async executeScript(): Promise<void> {
if (!this.isExecutionAllowed || this.isDisabled || this.isScriptEmpty) {
if (!this.isExecutionAllowed() || this.isDisabled || this.isScriptEmpty) {
return;
}
@@ -358,6 +370,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
commonDialogService,
sqlEditorSettingsService,
connectionInfoResource,
sqlEditorPermissionService,
},
);
@@ -0,0 +1,30 @@
/*
* CloudBeaver - Cloud Database Manager
* 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.
*/
import { EConnectionFeature, ConnectionInfoResource, type IConnectionInfoParams } from '@cloudbeaver/core-connections';
import { injectable } from '@cloudbeaver/core-di';
import { SqlEditorSettingsService } from './SqlEditorSettingsService.js';
@injectable(() => [ConnectionInfoResource, SqlEditorSettingsService])
export class SqlEditorPermissionService {
constructor(
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly sqlEditorSettingsService: SqlEditorSettingsService,
) {}
isScriptExecutionEnabled(connectionKey: IConnectionInfoParams): boolean {
const connection = this.connectionInfoResource.get(connectionKey);
if (!connection) {
return false;
}
return !connection.features.includes(EConnectionFeature.restrictScriptExecute) && this.sqlEditorSettingsService.scriptExecutionEnabled;
}
}
@@ -127,7 +127,7 @@ export class OutputMenuBootstrap extends Bootstrap {
const sqlDataSource = this.sqlDataSourceService.get(state.editorId);
const isQuery = sqlDataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = sqlEditorData?.isExecutionAllowed;
const isExecutable = sqlEditorData?.isExecutionAllowed();
return !!isQuery && !!isExecutable;
},
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 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.
@@ -27,9 +27,9 @@ import type { IResultGroup, ISqlEditorTabState } from '../ISqlEditorTabState.js'
import { QueryDataSource } from '../QueryDataSource.js';
import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService.js';
import { SqlQueryResultService } from './SqlQueryResultService.js';
import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js';
import { Executor, type IExecutor } from '@cloudbeaver/core-executor';
import { CommonDialogService } from '@cloudbeaver/core-dialogs';
import { SqlEditorPermissionService } from '../SqlEditorPermissionService.js';
interface IQueryExecutionOptions {
onQueryExecutionStart?: (query: string, index: number) => void;
@@ -57,9 +57,9 @@ export interface IQueryExecutionStatistics {
DataViewerService,
SqlDataSourceService,
DataViewerSettingsService,
SqlEditorSettingsService,
CommonDialogService,
AsyncTaskInfoEventHandler,
SqlEditorPermissionService,
])
export class SqlQueryService {
private readonly statisticsMap: Map<string, IQueryExecutionStatistics>;
@@ -78,9 +78,9 @@ export class SqlQueryService {
private readonly dataViewerService: DataViewerService,
private readonly sqlDataSourceService: SqlDataSourceService,
private readonly dataViewerSettingsService: DataViewerSettingsService,
private readonly sqlEditorSettingsService: SqlEditorSettingsService,
private readonly commonDialogService: CommonDialogService,
private readonly asyncTaskInfoEventHandler: AsyncTaskInfoEventHandler,
private readonly sqlEditorPermissionService: SqlEditorPermissionService,
) {
this.statisticsMap = new Map();
this.onQueryExecution = new Executor();
@@ -143,10 +143,6 @@ export class SqlQueryService {
try {
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
if (!this.sqlEditorSettingsService.scriptExecutionEnabled) {
throw new Error('Script execution is not allowed');
}
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
@@ -155,10 +151,14 @@ export class SqlQueryService {
return;
}
let model: IDatabaseDataModel<QueryDataSource>;
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
if (!this.sqlEditorPermissionService.isScriptExecutionEnabled(connectionKey)) {
throw new Error('Script execution is not allowed');
}
let model: IDatabaseDataModel<QueryDataSource>;
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
tabGroup = this.sqlQueryResultService.getSelectedGroup(editorState);
@@ -224,10 +224,6 @@ export class SqlQueryService {
try {
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
if (!this.sqlEditorSettingsService.scriptExecutionEnabled) {
throw new Error('Script execution is not allowed');
}
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
@@ -236,9 +232,14 @@ export class SqlQueryService {
return;
}
const groupNameOrder = this.sqlQueryResultService.getGroupNameOrder(editorState);
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
if (!this.sqlEditorPermissionService.isScriptExecutionEnabled(connectionKey)) {
throw new Error('Script execution is not allowed');
}
const groupNameOrder = this.sqlQueryResultService.getGroupNameOrder(editorState);
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
const statisticsTab = this.sqlQueryResultService.createStatisticsTab(editorState);
@@ -53,3 +53,4 @@ export * from './SqlEditorSettingsService.js';
export * from './SqlEditorView.js';
export * from './SqlResultTabs/SqlQueryService.js';
export * from './downloadSql.js';
export * from './SqlEditorPermissionService.js';
@@ -26,6 +26,7 @@ import { MenuBootstrap } from './MenuBootstrap.js';
import { LocaleService } from './LocaleService.js';
import { SqlEditorModel } from './SqlEditorModel/SqlEditorModel.js';
import { SqlEditorModelService } from './SqlEditorModel/SqlEditorModelService.js';
import { SqlEditorPermissionService } from './SqlEditorPermissionService.js';
export default ModuleRegistry.add({
name: '@cloudbeaver/plugin-sql-editor',
@@ -56,6 +57,7 @@ export default ModuleRegistry.add({
.addSingleton(SqlDataSourceService)
.addSingleton(LocalStorageSqlDataSourceBootstrap)
.addSingleton(SqlEditorModelService)
.addSingleton(MenuBootstrap);
.addSingleton(MenuBootstrap)
.addSingleton(SqlEditorPermissionService);
},
});