mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 14:50:15 +08:00
Merge branch 'devel' into dbeaver/pro#10157-fix-readable-error-for-cloud
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
|
||||
+10
-2
@@ -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())) {
|
||||
|
||||
+14
-4
@@ -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,
|
||||
|
||||
+5
@@ -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>
|
||||
);
|
||||
|
||||
+20
-18
@@ -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 && (
|
||||
|
||||
+11
-6
@@ -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} />,
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user