SYNC: Merge pull request #8 from dbgate/feature/db-table-permissions

This commit is contained in:
Jan Prochazka
2025-08-22 09:45:32 +02:00
committed by Diflow
parent f48b4a6c62
commit d2d6e2f554
28 changed files with 1316 additions and 277 deletions

View File

@@ -167,7 +167,7 @@ await dbgateApi.deployDb(${JSON.stringify(
isProApp() && { text: 'Data deployer', onClick: handleOpenDataDeployTab },
$currentDatabase && [
{ text: 'Generate deploy DB SQL', onClick: handleGenerateDeploySql },
{ text: 'Shell: Deploy DB', onClick: handleGenerateDeployScript },
hasPermission(`run-shell-script`) && { text: 'Shell: Deploy DB', onClick: handleGenerateDeployScript },
],
data.name != 'default' &&
isProApp() &&

View File

@@ -382,7 +382,8 @@
$extensions,
$currentDatabase,
$apps,
$openedSingleDatabaseConnections
$openedSingleDatabaseConnections,
data.databasePermissionRole,
),
],

View File

@@ -46,7 +46,8 @@
$extensions,
$currentDatabase,
$apps,
$openedSingleDatabaseConnections
$openedSingleDatabaseConnections,
databasePermissionRole
) {
const apps = filterAppsForDatabase(connection, name, $apps);
const handleNewQuery = () => {
@@ -412,11 +413,12 @@ await dbgateApi.executeQuery(${JSON.stringify(
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
return [
hasPermission(`dbops/query`) && {
onClick: handleNewQuery,
text: _t('database.newQuery', { defaultMessage: 'New query' }),
isNewQuery: true,
},
hasPermission(`dbops/query`) &&
isAllowedDatabaseRunScript(databasePermissionRole) && {
onClick: handleNewQuery,
text: _t('database.newQuery', { defaultMessage: 'New query' }),
isNewQuery: true,
},
hasPermission(`dbops/model/edit`) &&
!connection.isReadOnly &&
driver?.databaseEngineTypes?.includes('sql') && {
@@ -545,12 +547,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
{ divider: true },
driver?.databaseEngineTypes?.includes('sql') &&
hasPermission(`run-shell-script`) &&
hasPermission(`dbops/dropdb`) && {
onClick: handleGenerateDropAllObjectsScript,
text: _t('database.shellDropAllObjects', { defaultMessage: 'Shell: Drop all objects' }),
},
{
hasPermission(`run-shell-script`) && {
onClick: handleGenerateRunScript,
text: _t('database.shellRunScript', { defaultMessage: 'Shell: Run script' }),
},
@@ -625,7 +628,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
import NewCollectionModal from '../modals/NewCollectionModal.svelte';
import hasPermission from '../utility/hasPermission';
import hasPermission, { isAllowedDatabaseRunScript } from '../utility/hasPermission';
import { openImportExportTab } from '../utility/importExportTools';
import newTable from '../tableeditor/newTable';
import { loadSchemaList, switchCurrentDatabase } from '../utility/common';
@@ -636,6 +639,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
import { getNumberIcon } from '../icons/FontIcon.svelte';
import { getDatabaseClickActionSetting } from '../settings/settingsTools';
import { _t } from '../translations';
import { dataGridRowHeight } from '../datagrid/DataGridRowHeightMeter.svelte';
export let data;
export let passProps;
@@ -647,7 +651,8 @@ await dbgateApi.executeQuery(${JSON.stringify(
$extensions,
$currentDatabase,
$apps,
$openedSingleDatabaseConnections
$openedSingleDatabaseConnections,
data.databasePermissionRole
);
}
@@ -697,6 +702,9 @@ await dbgateApi.executeQuery(${JSON.stringify(
).length
)
: ''}
statusIconBefore={data.databasePermissionRole == 'read_content' || data.databasePermissionRole == 'view'
? 'icon lock'
: null}
menu={createMenu}
showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin}
onPin={isPinned ? null : () => pinnedDatabases.update(list => [...list, data])}

View File

@@ -703,15 +703,29 @@
}
function createMenus(objectTypeField, driver, data): ReturnType<typeof createMenusCore> {
return createMenusCore(objectTypeField, driver, data).filter(x => {
if (x.scriptTemplate) {
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
const coreMenus = createMenusCore(objectTypeField, driver, data);
const filteredSumenus = coreMenus.map(item => {
if (!item.submenu) {
return item;
}
if (x.sqlGeneratorProps) {
return hasPermission(`dbops/sql-generator`);
}
return true;
return {
...item,
submenu: item.submenu.filter(x => {
if (x.scriptTemplate) {
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
}
if (x.sqlGeneratorProps) {
return hasPermission(`dbops/sql-generator`);
}
return true;
}),
};
});
const filteredNoEmptySubmenus = filteredSumenus.filter(x => !x.submenu || x.submenu.length > 0);
return filteredNoEmptySubmenus;
}
function getObjectTitle(connection, schemaName, pureName) {
@@ -1062,6 +1076,7 @@
: null}
extInfo={getExtInfo(data)}
isChoosed={matchDatabaseObjectAppObject($selectedDatabaseObjectAppObject, data)}
statusIconBefore={data.tablePermissionRole == 'read' ? 'icon lock' : null}
on:click={() => handleObjectClick(data, 'leftClick')}
on:middleclick={() => handleObjectClick(data, 'middleClick')}
on:dblclick={() => handleObjectClick(data, 'dblClick')}

View File

@@ -322,6 +322,7 @@ registerCommand({
toolbar: true,
toolbarName: 'New table',
testEnabled: () => {
if (!hasPermission('dbops/model/edit')) return false;
const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions());
return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('sql');
},
@@ -671,7 +672,7 @@ registerCommand({
name: 'Export database',
toolbar: true,
icon: 'icon export',
testEnabled: () => getCurrentDatabase() != null,
testEnabled: () => getCurrentDatabase() != null && hasPermission(`dbops/export`),
onClick: () => {
openImportExportTab({
targetStorageType: getDefaultFileFormat(getExtensions()).storageType,
@@ -691,7 +692,8 @@ if (isProApp()) {
icon: 'icon compare',
testEnabled: () =>
getCurrentDatabase() != null &&
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql'),
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('sql')
&& hasPermission(`dbops/export`),
onClick: () => {
openNewTab(
{

View File

@@ -77,7 +77,10 @@
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
$serverVersion,
table => getDictionaryDescription(table, conid, database, $apps, $connections),
forceReadOnly || $connection?.isReadOnly,
forceReadOnly ||
$connection?.isReadOnly ||
extendedDbInfo?.tables?.find(x => x.pureName == pureName && x.schemaName == schemaName)
?.tablePermissionRole == 'read',
isRawMode,
$settingsValue
)

View File

@@ -74,6 +74,8 @@
'icon arrow-link': 'mdi mdi-arrow-top-right-thick',
'icon reset': 'mdi mdi-cancel',
'icon send': 'mdi mdi-send',
'icon regex': 'mdi mdi-regex',
'icon list': 'mdi mdi-format-list-bulleted-triangle',
'icon window-restore': 'mdi mdi-window-restore',
'icon window-maximize': 'mdi mdi-window-maximize',

View File

@@ -3,6 +3,7 @@
import runCommand from '../commands/runCommand';
import newQuery from '../query/newQuery';
import { commandsCustomized, selectedWidget } from '../stores';
import hasPermission from '../utility/hasPermission';
import { isProApp } from '../utility/proTools';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
@@ -19,6 +20,7 @@
newQuery({ multiTabIndex });
},
testid: 'NewObjectModal_query',
testEnabled: () => hasPermission('dbops/query'),
},
{
icon: 'icon connection',
@@ -114,7 +116,7 @@
isProFeature: true,
disabledMessage: 'Database chat is not available for current database',
testid: 'NewObjectModal_databaseChat',
}
},
];
</script>
@@ -122,7 +124,11 @@
<div class="create-header">Create new</div>
<div class="wrapper">
{#each NEW_ITEMS as item}
{@const enabled = item.command ? $commandsCustomized[item.command]?.enabled : true}
{@const enabled = item.command
? $commandsCustomized[item.command]?.enabled
: item.testEnabled
? item.testEnabled()
: true}
<NewObjectButton
icon={item.icon}
title={item.title}

View File

@@ -40,7 +40,7 @@
execute: true,
toggleComment: true,
findReplace: true,
executeAdditionalCondition: () => getCurrentEditor()?.hasConnection(),
executeAdditionalCondition: () => getCurrentEditor()?.hasConnection() && hasPermission('dbops/query'),
copyPaste: true,
});
registerCommand({

View File

@@ -80,7 +80,7 @@
import invalidateCommands from '../commands/invalidateCommands';
import { showModal } from '../modals/modalTools';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import { getTableInfo, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import { scriptToSql } from 'dbgate-sqltree';
import { extensions, lastUsedDefaultActions } from '../stores';
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
@@ -156,30 +156,47 @@
}
}
export function save() {
export async function save() {
const driver = findEngineDriver($connection, $extensions);
const tablePermissionRole = (await getTableInfo({ conid, database, schemaName, pureName }))?.tablePermissionRole;
const script = driver.createSaveChangeSetScript($changeSetStore?.value, $dbinfo, () =>
changeSetToSql($changeSetStore?.value, $dbinfo, driver.dialect)
);
const deleteCascades = getDeleteCascades($changeSetStore?.value, $dbinfo);
const sql = scriptToSql(driver, script);
const deleteCascadesScripts = _.map(deleteCascades, ({ title, commands }) => ({
title,
script: scriptToSql(driver, commands),
}));
// console.log('deleteCascadesScripts', deleteCascadesScripts);
if (getBoolSettingsValue('skipConfirm.tableDataSave', false) && !deleteCascadesScripts?.length) {
handleConfirmSql(sql);
} else {
showModal(ConfirmSqlModal, {
sql,
onConfirm: confirmedSql => handleConfirmSql(confirmedSql),
engine: driver.engine,
deleteCascadesScripts,
skipConfirmSettingKey: deleteCascadesScripts?.length ? null : 'skipConfirm.tableDataSave',
if (tablePermissionRole == 'create_update_delete' || tablePermissionRole == 'update_only') {
const resp = await apiCall('database-connections/save-table-data', {
conid,
database,
changeSet: $changeSetStore?.value,
});
const { errorMessage } = resp || {};
if (errorMessage) {
showModal(ErrorMessageModal, { title: 'Error when saving', message: errorMessage });
} else {
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
cache.update(reloadDataCacheFunc);
showSnackbarSuccess('Saved to database');
}
} else {
const script = driver.createSaveChangeSetScript($changeSetStore?.value, $dbinfo, () =>
changeSetToSql($changeSetStore?.value, $dbinfo, driver.dialect)
);
const deleteCascades = getDeleteCascades($changeSetStore?.value, $dbinfo);
const sql = scriptToSql(driver, script);
const deleteCascadesScripts = _.map(deleteCascades, ({ title, commands }) => ({
title,
script: scriptToSql(driver, commands),
}));
// console.log('deleteCascadesScripts', deleteCascadesScripts);
if (getBoolSettingsValue('skipConfirm.tableDataSave', false) && !deleteCascadesScripts?.length) {
handleConfirmSql(sql);
} else {
showModal(ConfirmSqlModal, {
sql,
onConfirm: confirmedSql => handleConfirmSql(confirmedSql),
engine: driver.engine,
deleteCascadesScripts,
skipConfirmSettingKey: deleteCascadesScripts?.length ? null : 'skipConfirm.tableDataSave',
});
}
}
}

View File

@@ -22,3 +22,8 @@ export function subscribePermissionCompiler() {
export function setConfigForPermissions(config) {
compiled = compilePermissions(config?.permissions || []);
}
export function isAllowedDatabaseRunScript(databasePermissionRole) {
return !databasePermissionRole || databasePermissionRole == 'run_script';
}