From d2d6e2f554611e76ce5558dabd4910e8a7d40b49 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Fri, 22 Aug 2025 09:45:32 +0200 Subject: [PATCH] SYNC: Merge pull request #8 from dbgate/feature/db-table-permissions --- packages/api/.env | 1 + packages/api/src/auth/authProvider.js | 14 +- packages/api/src/controllers/auth.js | 3 +- packages/api/src/controllers/config.js | 13 +- packages/api/src/controllers/connections.js | 15 +- .../src/controllers/databaseConnections.js | 209 ++++-- packages/api/src/controllers/files.js | 40 +- packages/api/src/controllers/plugins.js | 11 +- packages/api/src/controllers/runners.js | 3 + packages/api/src/controllers/scheduler.js | 5 +- .../api/src/controllers/serverConnections.js | 45 +- packages/api/src/controllers/sessions.js | 5 +- .../api/src/proc/databaseConnectionProcess.js | 25 +- packages/api/src/storageModel.js | 669 +++++++++++++++++- packages/api/src/utility/hasPermission.js | 345 +++++++-- packages/tools/src/testPermission.ts | 10 +- packages/types/dbinfo.d.ts | 29 +- .../src/appobj/ArchiveFolderAppObject.svelte | 2 +- .../web/src/appobj/ConnectionAppObject.svelte | 3 +- .../web/src/appobj/DatabaseAppObject.svelte | 26 +- .../src/appobj/DatabaseObjectAppObject.svelte | 29 +- packages/web/src/commands/stdCommands.ts | 6 +- .../web/src/datagrid/TableDataGrid.svelte | 5 +- packages/web/src/icons/FontIcon.svelte | 2 + packages/web/src/modals/NewObjectModal.svelte | 10 +- packages/web/src/tabs/QueryTab.svelte | 2 +- packages/web/src/tabs/TableDataTab.svelte | 61 +- packages/web/src/utility/hasPermission.ts | 5 + 28 files changed, 1316 insertions(+), 277 deletions(-) diff --git a/packages/api/.env b/packages/api/.env index 9ab8fc3d2..2cfdd13f7 100644 --- a/packages/api/.env +++ b/packages/api/.env @@ -2,6 +2,7 @@ DEVMODE=1 SHELL_SCRIPTING=1 ALLOW_DBGATE_PRIVATE_CLOUD=1 DEVWEB=1 +LOCAL_AUTH_PROXY=1 # LOCAL_AI_GATEWAY=true # REDIRECT_TO_DBGATE_CLOUD_LOGIN=1 diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 8e65b6129..3c4d1d3d6 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -36,12 +36,24 @@ class AuthProviderBase { return !!req?.user || !!req?.auth; } - getCurrentPermissions(req) { + async getCurrentPermissions(req) { const login = this.getCurrentLogin(req); const permissions = process.env[`LOGIN_PERMISSIONS_${login}`]; return permissions || process.env.PERMISSIONS; } + async checkCurrentConnectionPermission(req, conid) { + return true; + } + + async getCurrentDatabasePermissions(req) { + return []; + } + + async getCurrentTablePermissions(req) { + return []; + } + getLoginPageConnections() { return null; } diff --git a/packages/api/src/controllers/auth.js b/packages/api/src/controllers/auth.js index ae6e144b3..320e793f7 100644 --- a/packages/api/src/controllers/auth.js +++ b/packages/api/src/controllers/auth.js @@ -51,6 +51,7 @@ function authMiddleware(req, res, next) { '/auth/oauth-token', '/auth/login', '/auth/redirect', + '/redirect', '/stream', '/storage/get-connections-for-login-page', '/storage/set-admin-password', @@ -139,9 +140,9 @@ module.exports = { const accessToken = jwt.sign( { login: 'superadmin', - permissions: await storage.loadSuperadminPermissions(), roleId: -3, licenseUid, + amoid: 'superadmin', }, getTokenSecret(), { diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 0706c320c..7f9b2547c 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -3,7 +3,7 @@ const os = require('os'); const path = require('path'); const axios = require('axios'); const { datadir, getLogsFilePath } = require('../utility/directories'); -const { hasPermission } = require('../utility/hasPermission'); +const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission'); const socket = require('../utility/socket'); const _ = require('lodash'); const AsyncLock = require('async-lock'); @@ -46,7 +46,7 @@ module.exports = { async get(_params, req) { const authProvider = getAuthProviderFromReq(req); const login = authProvider.getCurrentLogin(req); - const permissions = authProvider.getCurrentPermissions(req); + const permissions = await authProvider.getCurrentPermissions(req); const isUserLoggedIn = authProvider.isUserLoggedIn(req); const singleConid = authProvider.getSingleConnectionId(req); @@ -280,7 +280,8 @@ module.exports = { updateSettings_meta: true, async updateSettings(values, req) { - if (!hasPermission(`settings/change`, req)) return false; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`settings/change`, loadedPermissions)) return false; cachedSettingsValue = null; const res = await lock.acquire('settings', async () => { @@ -392,7 +393,8 @@ module.exports = { exportConnectionsAndSettings_meta: true, async exportConnectionsAndSettings(_params, req) { - if (!hasPermission(`admin/config`, req)) { + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`admin/config`, loadedPermissions)) { throw new Error('Permission denied: admin/config'); } @@ -416,7 +418,8 @@ module.exports = { importConnectionsAndSettings_meta: true, async importConnectionsAndSettings({ db }, req) { - if (!hasPermission(`admin/config`, req)) { + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`admin/config`, loadedPermissions)) { throw new Error('Permission denied: admin/config'); } diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index f29f96e64..971de8a85 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -14,7 +14,7 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase'); const processArgs = require('../utility/processArgs'); const { safeJsonParse, getLogger, extractErrorLogData } = require('dbgate-tools'); const platformInfo = require('../utility/platformInfo'); -const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission'); +const { connectionHasPermission, testConnectionPermission, loadPermissionsFromRequest } = require('../utility/hasPermission'); const pipeForkLogs = require('../utility/pipeForkLogs'); const requireEngineDriver = require('../utility/requireEngineDriver'); const { getAuthProviderById } = require('../auth/authProvider'); @@ -227,6 +227,7 @@ module.exports = { list_meta: true, async list(_params, req) { const storage = require('./storage'); + const loadedPermissions = await loadPermissionsFromRequest(req); const storageConnections = await storage.connections(req); if (storageConnections) { @@ -234,9 +235,9 @@ module.exports = { } if (portalConnections) { if (platformInfo.allowShellConnection) return portalConnections; - return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req)); + return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, loadedPermissions)); } - return (await this.datastore.find()).filter(x => connectionHasPermission(x, req)); + return (await this.datastore.find()).filter(x => connectionHasPermission(x, loadedPermissions)); }, async getUsedEngines() { @@ -375,7 +376,7 @@ module.exports = { update_meta: true, async update({ _id, values }, req) { if (portalConnections) return; - testConnectionPermission(_id, req); + await testConnectionPermission(_id, req); const res = await this.datastore.patch(_id, values); socket.emitChanged('connection-list-changed'); return res; @@ -392,7 +393,7 @@ module.exports = { updateDatabase_meta: true, async updateDatabase({ conid, database, values }, req) { if (portalConnections) return; - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const conn = await this.datastore.get(conid); let databases = (conn && conn.databases) || []; if (databases.find(x => x.name == database)) { @@ -410,7 +411,7 @@ module.exports = { delete_meta: true, async delete(connection, req) { if (portalConnections) return; - testConnectionPermission(connection, req); + await testConnectionPermission(connection, req); const res = await this.datastore.remove(connection._id); socket.emitChanged('connection-list-changed'); return res; @@ -452,7 +453,7 @@ module.exports = { _id: '__model', }; } - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.getCore({ conid, mask: true }); }, diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 9a89a87ba..f8d3f10ab 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -29,7 +29,7 @@ const generateDeploySql = require('../shell/generateDeploySql'); const { createTwoFilesPatch } = require('diff'); const diff2htmlPage = require('../utility/diff2htmlPage'); const processArgs = require('../utility/processArgs'); -const { testConnectionPermission } = require('../utility/hasPermission'); +const { testConnectionPermission, hasPermission, loadPermissionsFromRequest, loadTablePermissionsFromRequest, getTablePermissionRole, loadDatabasePermissionsFromRequest, getDatabasePermissionRole, getTablePermissionRoleLevelIndex, testDatabaseRolePermission } = require('../utility/hasPermission'); const { MissingCredentialsError } = require('../utility/exceptions'); const pipeForkLogs = require('../utility/pipeForkLogs'); const crypto = require('crypto'); @@ -100,7 +100,7 @@ module.exports = { socket.emitChanged(`database-status-changed`, { conid, database }); }, - handle_ping() {}, + handle_ping() { }, // session event handlers @@ -235,7 +235,7 @@ module.exports = { queryData_meta: true, async queryData({ conid, database, sql }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); logger.info({ conid, database, sql }, 'DBGM-00007 Processing query'); const opened = await this.ensureOpened(conid, database); // if (opened && opened.status && opened.status.name == 'error') { @@ -247,7 +247,7 @@ module.exports = { sqlSelect_meta: true, async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest( opened, @@ -256,24 +256,23 @@ module.exports = { auditLogger: auditLogSessionGroup && select?.from?.name?.pureName ? response => { - sendToAuditLog(req, { - category: 'dbop', - component: 'DatabaseConnectionsController', - event: 'sql.select', - action: 'select', - severity: 'info', - conid, - database, - schemaName: select?.from?.name?.schemaName, - pureName: select?.from?.name?.pureName, - sumint1: response?.rows?.length, - sessionParam: `${conid}::${database}::${select?.from?.name?.schemaName || '0'}::${ - select?.from?.name?.pureName + sendToAuditLog(req, { + category: 'dbop', + component: 'DatabaseConnectionsController', + event: 'sql.select', + action: 'select', + severity: 'info', + conid, + database, + schemaName: select?.from?.name?.schemaName, + pureName: select?.from?.name?.pureName, + sumint1: response?.rows?.length, + sessionParam: `${conid}::${database}::${select?.from?.name?.schemaName || '0'}::${select?.from?.name?.pureName }`, - sessionGroup: auditLogSessionGroup, - message: `Loaded table data from ${select?.from?.name?.pureName}`, - }); - } + sessionGroup: auditLogSessionGroup, + message: `Loaded table data from ${select?.from?.name?.pureName}`, + }); + } : null, } ); @@ -282,7 +281,9 @@ module.exports = { runScript_meta: true, async runScript({ conid, database, sql, useTransaction, logMessage }, req) { - testConnectionPermission(conid, req); + const loadedPermissions = await loadPermissionsFromRequest(req); + await testConnectionPermission(conid, req, loadedPermissions); + await testDatabaseRolePermission(conid, database, 'run_script', req); logger.info({ conid, database, sql }, 'DBGM-00008 Processing script'); const opened = await this.ensureOpened(conid, database); sendToAuditLog(req, { @@ -303,7 +304,7 @@ module.exports = { runOperation_meta: true, async runOperation({ conid, database, operation, useTransaction }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); logger.info({ conid, database, operation }, 'DBGM-00009 Processing operation'); sendToAuditLog(req, { @@ -325,7 +326,7 @@ module.exports = { collectionData_meta: true, async collectionData({ conid, database, options, auditLogSessionGroup }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest( opened, @@ -334,21 +335,21 @@ module.exports = { auditLogger: auditLogSessionGroup && options?.pureName ? response => { - sendToAuditLog(req, { - category: 'dbop', - component: 'DatabaseConnectionsController', - event: 'nosql.collectionData', - action: 'select', - severity: 'info', - conid, - database, - pureName: options?.pureName, - sumint1: response?.result?.rows?.length, - sessionParam: `${conid}::${database}::${options?.pureName}`, - sessionGroup: auditLogSessionGroup, - message: `Loaded collection data ${options?.pureName}`, - }); - } + sendToAuditLog(req, { + category: 'dbop', + component: 'DatabaseConnectionsController', + event: 'nosql.collectionData', + action: 'select', + severity: 'info', + conid, + database, + pureName: options?.pureName, + sumint1: response?.result?.rows?.length, + sessionParam: `${conid}::${database}::${options?.pureName}`, + sessionGroup: auditLogSessionGroup, + message: `Loaded collection data ${options?.pureName}`, + }); + } : null, } ); @@ -356,7 +357,7 @@ module.exports = { }, async loadDataCore(msgtype, { conid, database, ...args }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype, ...args }); if (res.errorMessage) { @@ -371,7 +372,7 @@ module.exports = { schemaList_meta: true, async schemaList({ conid, database }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('schemaList', { conid, database }); }, @@ -383,43 +384,43 @@ module.exports = { loadKeys_meta: true, async loadKeys({ conid, database, root, filter, limit }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('loadKeys', { conid, database, root, filter, limit }); }, scanKeys_meta: true, async scanKeys({ conid, database, root, pattern, cursor, count }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('scanKeys', { conid, database, root, pattern, cursor, count }); }, exportKeys_meta: true, async exportKeys({ conid, database, options }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('exportKeys', { conid, database, options }); }, loadKeyInfo_meta: true, async loadKeyInfo({ conid, database, key }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('loadKeyInfo', { conid, database, key }); }, loadKeyTableRange_meta: true, async loadKeyTableRange({ conid, database, key, cursor, count }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count }); }, loadFieldValues_meta: true, async loadFieldValues({ conid, database, schemaName, pureName, field, search, dataType }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search, dataType }); }, callMethod_meta: true, async callMethod({ conid, database, method, args }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); return this.loadDataCore('callMethod', { conid, database, method, args }); // const opened = await this.ensureOpened(conid, database); @@ -432,7 +433,8 @@ module.exports = { updateCollection_meta: true, async updateCollection({ conid, database, changeSet }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); + const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet }); if (res.errorMessage) { @@ -443,6 +445,36 @@ module.exports = { return res.result || null; }, + saveTableData_meta: true, + async saveTableData({ conid, database, changeSet }, req) { + await testConnectionPermission(conid, req); + + const databasePermissions = await loadDatabasePermissionsFromRequest(req); + const tablePermissions = await loadTablePermissionsFromRequest(req); + const fieldsAndRoles = [ + [changeSet.inserts, 'create_update_delete'], + [changeSet.deletes, 'create_update_delete'], + [changeSet.updates, 'update_only'], + ] + for (const [operations, requiredRole] of fieldsAndRoles) { + for (const operation of operations) { + const role = getTablePermissionRole(conid, database, 'tables', operation.schemaName, operation.pureName, tablePermissions, databasePermissions); + if (getTablePermissionRoleLevelIndex(role) < getTablePermissionRoleLevelIndex(requiredRole)) { + throw new Error('Permission not granted'); + } + } + } + + const opened = await this.ensureOpened(conid, database); + const res = await this.sendRequest(opened, { msgtype: 'saveTableData', changeSet }); + if (res.errorMessage) { + return { + errorMessage: res.errorMessage, + }; + } + return res.result || null; + }, + status_meta: true, async status({ conid, database }, req) { if (!conid) { @@ -451,7 +483,7 @@ module.exports = { message: 'No connection', }; } - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const existing = this.opened.find(x => x.conid == conid && x.database == database); if (existing) { return { @@ -474,7 +506,7 @@ module.exports = { ping_meta: true, async ping({ conid, database }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); let existing = this.opened.find(x => x.conid == conid && x.database == database); if (existing) { @@ -502,7 +534,7 @@ module.exports = { refresh_meta: true, async refresh({ conid, database, keepOpen }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); if (!keepOpen) this.close(conid, database); await this.ensureOpened(conid, database); @@ -516,7 +548,7 @@ module.exports = { return { status: 'ok' }; } - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const conn = await this.ensureOpened(conid, database); conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh }); return { status: 'ok' }; @@ -553,7 +585,7 @@ module.exports = { disconnect_meta: true, async disconnect({ conid, database }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); await this.close(conid, database, true); return { status: 'ok' }; }, @@ -563,8 +595,9 @@ module.exports = { if (!conid || !database) { return {}; } + const loadedPermissions = await loadPermissionsFromRequest(req); - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req, loadedPermissions); if (conid == '__model') { const model = await importDbModel(database); const trans = await loadModelTransform(modelTransFile); @@ -586,6 +619,38 @@ module.exports = { message: `Loaded database structure for ${database}`, }); + if (!hasPermission(`all-tables`, loadedPermissions)) { + // filter databases by permissions + const tablePermissions = await loadTablePermissionsFromRequest(req); + const databasePermissions = await loadDatabasePermissionsFromRequest(req); + const databasePermissionRole = getDatabasePermissionRole(conid, database, databasePermissions); + + function applyTablePermissionRole(list, objectTypeField) { + const res = []; + for (const item of list ?? []) { + const tablePermissionRole = getTablePermissionRole(conid, database, objectTypeField, item.schemaName, item.pureName, tablePermissions, databasePermissionRole); + if (tablePermissionRole != 'deny') { + res.push({ + ...item, + tablePermissionRole, + }); + } + } + return res; + } + + const res = { + ...opened.structure, + tables: applyTablePermissionRole(opened.structure.tables, 'tables'), + views: applyTablePermissionRole(opened.structure.views, 'views'), + procedures: applyTablePermissionRole(opened.structure.procedures, 'procedures'), + functions: applyTablePermissionRole(opened.structure.functions, 'functions'), + triggers: applyTablePermissionRole(opened.structure.triggers, 'triggers'), + collections: applyTablePermissionRole(opened.structure.collections, 'collections'), + } + return res; + } + return opened.structure; // const existing = this.opened.find((x) => x.conid == conid && x.database == database); // if (existing) return existing.status; @@ -600,7 +665,7 @@ module.exports = { if (!conid) { return null; } - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); if (!conid) return null; const opened = await this.ensureOpened(conid, database); return opened.serverVersion || null; @@ -608,7 +673,7 @@ module.exports = { sqlPreview_meta: true, async sqlPreview({ conid, database, objects, options }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); // wait for structure await this.structure({ conid, database }); @@ -619,7 +684,7 @@ module.exports = { exportModel_meta: true, async exportModel({ conid, database, outputFolder, schema }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const realFolder = outputFolder.startsWith('archive:') ? resolveArchiveFolder(outputFolder.substring('archive:'.length)) @@ -637,7 +702,7 @@ module.exports = { exportModelSql_meta: true, async exportModelSql({ conid, database, outputFolder, outputFile, schema }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const connection = await connections.getCore({ conid }); const driver = requireEngineDriver(connection); @@ -651,7 +716,7 @@ module.exports = { generateDeploySql_meta: true, async generateDeploySql({ conid, database, archiveFolder }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype: 'generateDeploySql', @@ -816,17 +881,17 @@ module.exports = { return { ...(command == 'backup' ? driver.backupDatabaseCommand( - connection, - { outputFile, database, options, selectedTables, skippedTables, argsFormat }, - // @ts-ignore - externalTools - ) + connection, + { outputFile, database, options, selectedTables, skippedTables, argsFormat }, + // @ts-ignore + externalTools + ) : driver.restoreDatabaseCommand( - connection, - { inputFile, database, options, argsFormat }, - // @ts-ignore - externalTools - )), + connection, + { inputFile, database, options, argsFormat }, + // @ts-ignore + externalTools + )), transformMessage: driver.transformNativeCommandMessage ? message => driver.transformNativeCommandMessage(message, command) : null, @@ -923,7 +988,7 @@ module.exports = { executeSessionQuery_meta: true, async executeSessionQuery({ sesid, conid, database, sql }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); logger.info({ sesid, sql }, 'DBGM-00010 Processing query'); sessions.dispatchMessage(sesid, 'Query execution started'); @@ -935,7 +1000,7 @@ module.exports = { evalJsonScript_meta: true, async evalJsonScript({ conid, database, script, runid }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); opened.subprocess.send({ msgtype: 'evalJsonScript', script, runid }); diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index 25003e941..4d776c73c 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -3,7 +3,7 @@ const path = require('path'); const crypto = require('crypto'); const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories'); const getChartExport = require('../utility/getChartExport'); -const { hasPermission } = require('../utility/hasPermission'); +const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission'); const socket = require('../utility/socket'); const scheduler = require('./scheduler'); const getDiagramExport = require('../utility/getDiagramExport'); @@ -31,7 +31,8 @@ function deserialize(format, text) { module.exports = { list_meta: true, async list({ folder }, req) { - if (!hasPermission(`files/${folder}/read`, req)) return []; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return []; const dir = path.join(filesdir(), folder); if (!(await fs.exists(dir))) return []; const files = (await fs.readdir(dir)).map(file => ({ folder, file })); @@ -40,10 +41,11 @@ module.exports = { listAll_meta: true, async listAll(_params, req) { + const loadedPermissions = await loadPermissionsFromRequest(req); const folders = await fs.readdir(filesdir()); const res = []; for (const folder of folders) { - if (!hasPermission(`files/${folder}/read`, req)) continue; + if (!hasPermission(`files/${folder}/read`, loadedPermissions)) continue; const dir = path.join(filesdir(), folder); const files = (await fs.readdir(dir)).map(file => ({ folder, file })); res.push(...files); @@ -53,7 +55,8 @@ module.exports = { delete_meta: true, async delete({ folder, file }, req) { - if (!hasPermission(`files/${folder}/write`, req)) return false; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false; if (!checkSecureFilePathsWithoutDirectory(folder, file)) { return false; } @@ -65,7 +68,8 @@ module.exports = { rename_meta: true, async rename({ folder, file, newFile }, req) { - if (!hasPermission(`files/${folder}/write`, req)) return false; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false; if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) { return false; } @@ -86,10 +90,11 @@ module.exports = { copy_meta: true, async copy({ folder, file, newFile }, req) { + const loadedPermissions = await loadPermissionsFromRequest(req); if (!checkSecureFilePathsWithoutDirectory(folder, file, newFile)) { return false; } - if (!hasPermission(`files/${folder}/write`, req)) return false; + if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false; await fs.copyFile(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile)); socket.emitChanged(`files-changed`, { folder }); socket.emitChanged(`all-files-changed`); @@ -113,7 +118,8 @@ module.exports = { }); return deserialize(format, text); } else { - if (!hasPermission(`files/${folder}/read`, req)) return null; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return null; const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' }); return deserialize(format, text); } @@ -131,18 +137,19 @@ module.exports = { save_meta: true, async save({ folder, file, data, format }, req) { + const loadedPermissions = await loadPermissionsFromRequest(req); if (!checkSecureFilePathsWithoutDirectory(folder, file)) { return false; } if (folder.startsWith('archive:')) { - if (!hasPermission(`archive/write`, req)) return false; + if (!hasPermission(`archive/write`, loadedPermissions)) return false; const dir = resolveArchiveFolder(folder.substring('archive:'.length)); await fs.writeFile(path.join(dir, file), serialize(format, data)); socket.emitChanged(`archive-files-changed`, { folder: folder.substring('archive:'.length) }); return true; } else if (folder.startsWith('app:')) { - if (!hasPermission(`apps/write`, req)) return false; + if (!hasPermission(`apps/write`, loadedPermissions)) return false; const app = folder.substring('app:'.length); await fs.writeFile(path.join(appdir(), app, file), serialize(format, data)); socket.emitChanged(`app-files-changed`, { app }); @@ -150,7 +157,7 @@ module.exports = { apps.emitChangedDbApp(folder); return true; } else { - if (!hasPermission(`files/${folder}/write`, req)) return false; + if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false; const dir = path.join(filesdir(), folder); if (!(await fs.exists(dir))) { await fs.mkdir(dir); @@ -177,7 +184,8 @@ module.exports = { favorites_meta: true, async favorites(_params, req) { - if (!hasPermission(`files/favorites/read`, req)) return []; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`files/favorites/read`, loadedPermissions)) return []; const dir = path.join(filesdir(), 'favorites'); if (!(await fs.exists(dir))) return []; const files = await fs.readdir(dir); @@ -234,16 +242,17 @@ module.exports = { getFileRealPath_meta: true, async getFileRealPath({ folder, file }, req) { + const loadedPermissions = await loadPermissionsFromRequest(req); if (folder.startsWith('archive:')) { - if (!hasPermission(`archive/write`, req)) return false; + if (!hasPermission(`archive/write`, loadedPermissions)) return false; const dir = resolveArchiveFolder(folder.substring('archive:'.length)); return path.join(dir, file); } else if (folder.startsWith('app:')) { - if (!hasPermission(`apps/write`, req)) return false; + if (!hasPermission(`apps/write`, loadedPermissions)) return false; const app = folder.substring('app:'.length); return path.join(appdir(), app, file); } else { - if (!hasPermission(`files/${folder}/write`, req)) return false; + if (!hasPermission(`files/${folder}/write`, loadedPermissions)) return false; const dir = path.join(filesdir(), folder); if (!(await fs.exists(dir))) { await fs.mkdir(dir); @@ -297,7 +306,8 @@ module.exports = { exportFile_meta: true, async exportFile({ folder, file, filePath }, req) { - if (!hasPermission(`files/${folder}/read`, req)) return false; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return false; await fs.copyFile(path.join(filesdir(), folder, file), filePath); return true; }, diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index 2b74a4669..adf32ebf5 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -7,7 +7,7 @@ const socket = require('../utility/socket'); const compareVersions = require('compare-versions'); const requirePlugin = require('../shell/requirePlugin'); const downloadPackage = require('../utility/downloadPackage'); -const { hasPermission } = require('../utility/hasPermission'); +const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission'); const _ = require('lodash'); const packagedPluginsContent = require('../packagedPluginsContent'); @@ -118,7 +118,8 @@ module.exports = { install_meta: true, async install({ packageName }, req) { - if (!hasPermission(`plugins/install`, req)) return; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`plugins/install`, loadedPermissions)) return; const dir = path.join(pluginsdir(), packageName); // @ts-ignore if (!(await fs.exists(dir))) { @@ -132,7 +133,8 @@ module.exports = { uninstall_meta: true, async uninstall({ packageName }, req) { - if (!hasPermission(`plugins/install`, req)) return; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`plugins/install`, loadedPermissions)) return; const dir = path.join(pluginsdir(), packageName); await fs.rmdir(dir, { recursive: true }); socket.emitChanged(`installed-plugins-changed`); @@ -143,7 +145,8 @@ module.exports = { upgrade_meta: true, async upgrade({ packageName }, req) { - if (!hasPermission(`plugins/install`, req)) return; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission(`plugins/install`, loadedPermissions)) return; const dir = path.join(pluginsdir(), packageName); // @ts-ignore if (await fs.exists(dir)) { diff --git a/packages/api/src/controllers/runners.js b/packages/api/src/controllers/runners.js index 123280194..3ff917e6e 100644 --- a/packages/api/src/controllers/runners.js +++ b/packages/api/src/controllers/runners.js @@ -21,6 +21,7 @@ const processArgs = require('../utility/processArgs'); const platformInfo = require('../utility/platformInfo'); const { checkSecureDirectories, checkSecureDirectoriesInScript } = require('../utility/security'); const { sendToAuditLog, logJsonRunnerScript } = require('../utility/auditlog'); +const { testStandardPermission } = require('../utility/hasPermission'); const logger = getLogger('runners'); function extractPlugins(script) { @@ -273,6 +274,8 @@ module.exports = { start_meta: true, async start({ script }, req) { + await testStandardPermission('run-shell-script', req); + const runid = crypto.randomUUID(); if (script.type == 'json') { diff --git a/packages/api/src/controllers/scheduler.js b/packages/api/src/controllers/scheduler.js index bd8138c99..a8e1bab3c 100644 --- a/packages/api/src/controllers/scheduler.js +++ b/packages/api/src/controllers/scheduler.js @@ -3,7 +3,7 @@ const fs = require('fs-extra'); const path = require('path'); const cron = require('node-cron'); const runners = require('./runners'); -const { hasPermission } = require('../utility/hasPermission'); +const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission'); const { getLogger } = require('dbgate-tools'); const logger = getLogger('scheduler'); @@ -30,7 +30,8 @@ module.exports = { }, async reload(_params, req) { - if (!hasPermission('files/shell/read', req)) return; + const loadedPermissions = await loadPermissionsFromRequest(req); + if (!hasPermission('files/shell/read', loadedPermissions)) return; const shellDir = path.join(filesdir(), 'shell'); await this.unload(); if (!(await fs.exists(shellDir))) return; diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index db0bea5b6..c9f0f3fa1 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -8,7 +8,13 @@ const { handleProcessCommunication } = require('../utility/processComm'); const lock = new AsyncLock(); const config = require('./config'); const processArgs = require('../utility/processArgs'); -const { testConnectionPermission } = require('../utility/hasPermission'); +const { + testConnectionPermission, + loadPermissionsFromRequest, + hasPermission, + loadDatabasePermissionsFromRequest, + getDatabasePermissionRole, +} = require('../utility/hasPermission'); const { MissingCredentialsError } = require('../utility/exceptions'); const pipeForkLogs = require('../utility/pipeForkLogs'); const { getLogger, extractErrorLogData } = require('dbgate-tools'); @@ -40,7 +46,7 @@ module.exports = { existing.status = status; socket.emitChanged(`server-status-changed`); }, - handle_ping() {}, + handle_ping() { }, handle_response(conid, { msgid, ...response }) { const [resolve, reject] = this.requests[msgid]; resolve(response); @@ -135,7 +141,7 @@ module.exports = { disconnect_meta: true, async disconnect({ conid }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); await this.close(conid, true); return { status: 'ok' }; }, @@ -144,7 +150,9 @@ module.exports = { async listDatabases({ conid }, req) { if (!conid) return []; if (conid == '__model') return []; - testConnectionPermission(conid, req); + const loadedPermissions = await loadPermissionsFromRequest(req); + + await testConnectionPermission(conid, req, loadedPermissions); const opened = await this.ensureOpened(conid); sendToAuditLog(req, { category: 'serverop', @@ -157,12 +165,29 @@ module.exports = { sessionGroup: 'listDatabases', message: `Loaded databases for connection`, }); + + if (!hasPermission(`all-databases`, loadedPermissions)) { + // filter databases by permissions + const databasePermissions = await loadDatabasePermissionsFromRequest(req); + const res = []; + for (const db of opened?.databases ?? []) { + const databasePermissionRole = getDatabasePermissionRole(db.id, db.name, databasePermissions); + if (databasePermissionRole != 'deny') { + res.push({ + ...db, + databasePermissionRole, + }); + } + } + return res; + } + return opened?.databases ?? []; }, version_meta: true, async version({ conid }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); return opened?.version ?? null; }, @@ -202,7 +227,7 @@ module.exports = { refresh_meta: true, async refresh({ conid, keepOpen }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); if (!keepOpen) this.close(conid); await this.ensureOpened(conid); @@ -210,7 +235,7 @@ module.exports = { }, async sendDatabaseOp({ conid, msgtype, name }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); if (!opened) { return null; @@ -252,7 +277,7 @@ module.exports = { }, async loadDataCore(msgtype, { conid, ...args }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); if (!opened) { return null; @@ -270,8 +295,8 @@ module.exports = { serverSummary_meta: true, async serverSummary({ conid }, req) { + await testConnectionPermission(conid, req); logger.info({ conid }, 'DBGM-00260 Processing server summary'); - testConnectionPermission(conid, req); return this.loadDataCore('serverSummary', { conid }); }, @@ -306,7 +331,7 @@ module.exports = { summaryCommand_meta: true, async summaryCommand({ conid, command, row }, req) { - testConnectionPermission(conid, req); + await testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); if (!opened) { return null; diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index 3a3cd9b2b..5d5f9bc81 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -12,6 +12,7 @@ const { getLogger, extractErrorLogData } = require('dbgate-tools'); const pipeForkLogs = require('../utility/pipeForkLogs'); const config = require('./config'); const { sendToAuditLog } = require('../utility/auditlog'); +const { testStandardPermission, testDatabaseRolePermission } = require('../utility/hasPermission'); const logger = getLogger('sessions'); @@ -94,7 +95,7 @@ module.exports = { socket.emit(`session-initialize-file-${jslid}`); }, - handle_ping() {}, + handle_ping() { }, create_meta: true, async create({ conid, database }) { @@ -148,10 +149,12 @@ module.exports = { executeQuery_meta: true, async executeQuery({ sesid, sql, autoCommit, autoDetectCharts, limitRows, frontMatter }, req) { + await testStandardPermission('dbops/query', req); const session = this.opened.find(x => x.sesid == sesid); if (!session) { throw new Error('Invalid session'); } + await testDatabaseRolePermission(session.conid, session.database, 'run_script', req); sendToAuditLog(req, { category: 'dbop', diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index cf3b42639..f5994b83f 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -17,13 +17,14 @@ const requireEngineDriver = require('../utility/requireEngineDriver'); const { connectUtility } = require('../utility/connectUtility'); const { handleProcessCommunication } = require('../utility/processComm'); const generateDeploySql = require('../shell/generateDeploySql'); -const { dumpSqlSelect } = require('dbgate-sqltree'); +const { dumpSqlSelect, scriptToSql } = require('dbgate-sqltree'); const { allowExecuteCustomScript, handleQueryStream } = require('../utility/handleQueryStream'); const dbgateApi = require('../shell'); const requirePlugin = require('../shell/requirePlugin'); const path = require('path'); const { rundir } = require('../utility/directories'); const fs = require('fs-extra'); +const { changeSetToSql } = require('dbgate-datalib'); const logger = getLogger('dbconnProcess'); @@ -348,6 +349,27 @@ async function handleUpdateCollection({ msgid, changeSet }) { } } +async function handleSaveTableData({ msgid, changeSet }) { + await waitStructure(); + try { + const driver = requireEngineDriver(storedConnection); + const script = driver.createSaveChangeSetScript(changeSet, analysedStructure, () => + changeSetToSql(changeSet, analysedStructure, driver.dialect) + ); + const sql = scriptToSql(driver, script); + await driver.script(dbhan, sql, { useTransaction: true }); + process.send({ msgtype: 'response', msgid }); + } catch (err) { + process.send({ + msgtype: 'response', + msgid, + errorMessage: extractErrorMessage(err, 'Error executing SQL script'), + }); + } + + +} + async function handleSqlPreview({ msgid, objects, options }) { await waitStructure(); const driver = requireEngineDriver(storedConnection); @@ -464,6 +486,7 @@ const messageHandlers = { runScript: handleRunScript, runOperation: handleRunOperation, updateCollection: handleUpdateCollection, + saveTableData: handleSaveTableData, collectionData: handleCollectionData, loadKeys: handleLoadKeys, scanKeys: handleScanKeys, diff --git a/packages/api/src/storageModel.js b/packages/api/src/storageModel.js index 4086a5b43..88c1e8346 100644 --- a/packages/api/src/storageModel.js +++ b/packages/api/src/storageModel.js @@ -695,27 +695,27 @@ module.exports = { } }, { - "pureName": "roles", + "pureName": "database_permission_roles", "columns": [ { - "pureName": "roles", + "pureName": "database_permission_roles", "columnName": "id", "dataType": "int", "autoIncrement": true, "notNull": true }, { - "pureName": "roles", + "pureName": "database_permission_roles", "columnName": "name", - "dataType": "varchar(250)", - "notNull": false + "dataType": "varchar(100)", + "notNull": true } ], "foreignKeys": [], "primaryKey": { - "pureName": "roles", + "pureName": "database_permission_roles", "constraintType": "primaryKey", - "constraintName": "PK_roles", + "constraintName": "PK_database_permission_roles", "columns": [ { "columnName": "id" @@ -725,15 +725,23 @@ module.exports = { "preloadedRows": [ { "id": -1, - "name": "anonymous-user" + "name": "view" }, { "id": -2, - "name": "logged-user" + "name": "read_content" }, { "id": -3, - "name": "superadmin" + "name": "write_data" + }, + { + "id": -4, + "name": "run_script" + }, + { + "id": -5, + "name": "deny" } ] }, @@ -799,6 +807,98 @@ module.exports = { ] } }, + { + "pureName": "role_databases", + "columns": [ + { + "pureName": "role_databases", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "role_databases", + "columnName": "role_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "role_databases", + "columnName": "connection_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "role_databases", + "columnName": "database_names_list", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "role_databases", + "columnName": "database_names_regex", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "role_databases", + "columnName": "database_permission_role_id", + "dataType": "int", + "notNull": true + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_role_databases_role_id", + "pureName": "role_databases", + "refTableName": "roles", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "role_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_databases_connection_id", + "pureName": "role_databases", + "refTableName": "connections", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "connection_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_databases_database_permission_role_id", + "pureName": "role_databases", + "refTableName": "database_permission_roles", + "columns": [ + { + "columnName": "database_permission_role_id", + "refColumnName": "id" + } + ] + } + ], + "primaryKey": { + "pureName": "role_databases", + "constraintType": "primaryKey", + "constraintName": "PK_role_databases", + "columns": [ + { + "columnName": "id" + } + ] + } + }, { "pureName": "role_permissions", "columns": [ @@ -849,39 +949,132 @@ module.exports = { } }, { - "pureName": "users", + "pureName": "role_tables", "columns": [ { - "pureName": "users", + "pureName": "role_tables", "columnName": "id", "dataType": "int", "autoIncrement": true, "notNull": true }, { - "pureName": "users", - "columnName": "login", - "dataType": "varchar(250)", + "pureName": "role_tables", + "columnName": "role_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "role_tables", + "columnName": "connection_id", + "dataType": "int", "notNull": false }, { - "pureName": "users", - "columnName": "password", - "dataType": "varchar(250)", + "pureName": "role_tables", + "columnName": "database_names_list", + "dataType": "varchar(1000)", "notNull": false }, { - "pureName": "users", - "columnName": "email", - "dataType": "varchar(250)", + "pureName": "role_tables", + "columnName": "database_names_regex", + "dataType": "varchar(1000)", "notNull": false + }, + { + "pureName": "role_tables", + "columnName": "schema_names_list", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "role_tables", + "columnName": "schema_names_regex", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "role_tables", + "columnName": "table_names_list", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "role_tables", + "columnName": "table_names_regex", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "role_tables", + "columnName": "table_permission_role_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "role_tables", + "columnName": "table_permission_scope_id", + "dataType": "int", + "notNull": true + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_role_tables_role_id", + "pureName": "role_tables", + "refTableName": "roles", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "role_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_tables_connection_id", + "pureName": "role_tables", + "refTableName": "connections", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "connection_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_tables_table_permission_role_id", + "pureName": "role_tables", + "refTableName": "table_permission_roles", + "columns": [ + { + "columnName": "table_permission_role_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_tables_table_permission_scope_id", + "pureName": "role_tables", + "refTableName": "table_permission_scopes", + "columns": [ + { + "columnName": "table_permission_scope_id", + "refColumnName": "id" + } + ] } ], - "foreignKeys": [], "primaryKey": { - "pureName": "users", + "pureName": "role_tables", "constraintType": "primaryKey", - "constraintName": "PK_users", + "constraintName": "PK_role_tables", "columns": [ { "columnName": "id" @@ -889,6 +1082,167 @@ module.exports = { ] } }, + { + "pureName": "roles", + "columns": [ + { + "pureName": "roles", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "roles", + "columnName": "name", + "dataType": "varchar(250)", + "notNull": false + } + ], + "foreignKeys": [], + "primaryKey": { + "pureName": "roles", + "constraintType": "primaryKey", + "constraintName": "PK_roles", + "columns": [ + { + "columnName": "id" + } + ] + }, + "preloadedRows": [ + { + "id": -1, + "name": "anonymous-user" + }, + { + "id": -2, + "name": "logged-user" + }, + { + "id": -3, + "name": "superadmin" + } + ] + }, + { + "pureName": "table_permission_roles", + "columns": [ + { + "pureName": "table_permission_roles", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "table_permission_roles", + "columnName": "name", + "dataType": "varchar(100)", + "notNull": true + } + ], + "foreignKeys": [], + "primaryKey": { + "pureName": "table_permission_roles", + "constraintType": "primaryKey", + "constraintName": "PK_table_permission_roles", + "columns": [ + { + "columnName": "id" + } + ] + }, + "preloadedRows": [ + { + "id": -1, + "name": "read" + }, + { + "id": -2, + "name": "update_only" + }, + { + "id": -3, + "name": "create_update_delete" + }, + { + "id": -4, + "name": "run_script" + }, + { + "id": -5, + "name": "deny" + } + ] + }, + { + "pureName": "table_permission_scopes", + "columns": [ + { + "pureName": "table_permission_scopes", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "table_permission_scopes", + "columnName": "name", + "dataType": "varchar(100)", + "notNull": true + } + ], + "foreignKeys": [], + "primaryKey": { + "pureName": "table_permission_scopes", + "constraintType": "primaryKey", + "constraintName": "PK_table_permission_scopes", + "columns": [ + { + "columnName": "id" + } + ] + }, + "preloadedRows": [ + { + "id": -1, + "name": "all_objects" + }, + { + "id": -2, + "name": "tables" + }, + { + "id": -3, + "name": "views" + }, + { + "id": -4, + "name": "tables_views_collections" + }, + { + "id": -5, + "name": "procedures" + }, + { + "id": -6, + "name": "functions" + }, + { + "id": -7, + "name": "triggers" + }, + { + "id": -8, + "name": "sql_objects" + }, + { + "id": -9, + "name": "collections" + } + ] + }, { "pureName": "user_connections", "columns": [ @@ -951,6 +1305,98 @@ module.exports = { ] } }, + { + "pureName": "user_databases", + "columns": [ + { + "pureName": "user_databases", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "user_databases", + "columnName": "user_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "user_databases", + "columnName": "connection_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "user_databases", + "columnName": "database_names_list", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_databases", + "columnName": "database_names_regex", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_databases", + "columnName": "database_permission_role_id", + "dataType": "int", + "notNull": true + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_user_databases_user_id", + "pureName": "user_databases", + "refTableName": "users", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "user_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_user_databases_connection_id", + "pureName": "user_databases", + "refTableName": "connections", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "connection_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_user_databases_database_permission_role_id", + "pureName": "user_databases", + "refTableName": "database_permission_roles", + "columns": [ + { + "columnName": "database_permission_role_id", + "refColumnName": "id" + } + ] + } + ], + "primaryKey": { + "pureName": "user_databases", + "constraintType": "primaryKey", + "constraintName": "PK_user_databases", + "columns": [ + { + "columnName": "id" + } + ] + } + }, { "pureName": "user_permissions", "columns": [ @@ -1061,6 +1507,181 @@ module.exports = { } ] } + }, + { + "pureName": "user_tables", + "columns": [ + { + "pureName": "user_tables", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "user_tables", + "columnName": "user_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "user_tables", + "columnName": "connection_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "user_tables", + "columnName": "database_names_list", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_tables", + "columnName": "database_names_regex", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_tables", + "columnName": "schema_names_list", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_tables", + "columnName": "schema_names_regex", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_tables", + "columnName": "table_names_list", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_tables", + "columnName": "table_names_regex", + "dataType": "varchar(1000)", + "notNull": false + }, + { + "pureName": "user_tables", + "columnName": "table_permission_role_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "user_tables", + "columnName": "table_permission_scope_id", + "dataType": "int", + "notNull": true + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_user_tables_user_id", + "pureName": "user_tables", + "refTableName": "users", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "user_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_user_tables_connection_id", + "pureName": "user_tables", + "refTableName": "connections", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "connection_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_user_tables_table_permission_role_id", + "pureName": "user_tables", + "refTableName": "table_permission_roles", + "columns": [ + { + "columnName": "table_permission_role_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_user_tables_table_permission_scope_id", + "pureName": "user_tables", + "refTableName": "table_permission_scopes", + "columns": [ + { + "columnName": "table_permission_scope_id", + "refColumnName": "id" + } + ] + } + ], + "primaryKey": { + "pureName": "user_tables", + "constraintType": "primaryKey", + "constraintName": "PK_user_tables", + "columns": [ + { + "columnName": "id" + } + ] + } + }, + { + "pureName": "users", + "columns": [ + { + "pureName": "users", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "users", + "columnName": "login", + "dataType": "varchar(250)", + "notNull": false + }, + { + "pureName": "users", + "columnName": "password", + "dataType": "varchar(250)", + "notNull": false + }, + { + "pureName": "users", + "columnName": "email", + "dataType": "varchar(250)", + "notNull": false + } + ], + "foreignKeys": [], + "primaryKey": { + "pureName": "users", + "constraintType": "primaryKey", + "constraintName": "PK_users", + "columns": [ + { + "columnName": "id" + } + ] + } } ], "collections": [], diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js index 654ba8ceb..043d44fb0 100644 --- a/packages/api/src/utility/hasPermission.js +++ b/packages/api/src/utility/hasPermission.js @@ -1,101 +1,302 @@ -const { compilePermissions, testPermission } = require('dbgate-tools'); +const { compilePermissions, testPermission, getPermissionsCacheKey } = require('dbgate-tools'); const _ = require('lodash'); const { getAuthProviderFromReq } = require('../auth/authProvider'); const cachedPermissions = {}; -function hasPermission(tested, req) { +async function loadPermissionsFromRequest(req) { + const authProvider = getAuthProviderFromReq(req); if (!req) { - // request object not available, allow all + return null; + } + + const loadedPermissions = await authProvider.getCurrentPermissions(req); + return loadedPermissions; +} + +function hasPermission(tested, loadedPermissions) { + if (!loadedPermissions) { + // not available, allow all return true; } - const permissions = getAuthProviderFromReq(req).getCurrentPermissions(req); - - if (!cachedPermissions[permissions]) { - cachedPermissions[permissions] = compilePermissions(permissions); + const permissionsKey = getPermissionsCacheKey(loadedPermissions); + if (!cachedPermissions[permissionsKey]) { + cachedPermissions[permissionsKey] = compilePermissions(loadedPermissions); } - return testPermission(tested, cachedPermissions[permissions]); - - // const { user } = (req && req.auth) || {}; - // const { login } = (process.env.OAUTH_PERMISSIONS && req && req.user) || {}; - // const key = user || login || ''; - // const logins = getLogins(); - - // if (!userPermissions[key]) { - // if (logins) { - // const login = logins.find(x => x.login == user); - // userPermissions[key] = compilePermissions(login ? login.permissions : null); - // } else { - // userPermissions[key] = compilePermissions(process.env.PERMISSIONS); - // } - // } - // return testPermission(tested, userPermissions[key]); + return testPermission(tested, cachedPermissions[permissionsKey]); } -// let loginsCache = null; -// let loginsLoaded = false; - -// function getLogins() { -// if (loginsLoaded) { -// return loginsCache; -// } - -// const res = []; -// if (process.env.LOGIN && process.env.PASSWORD) { -// res.push({ -// login: process.env.LOGIN, -// password: process.env.PASSWORD, -// permissions: process.env.PERMISSIONS, -// }); -// } -// if (process.env.LOGINS) { -// const logins = _.compact(process.env.LOGINS.split(',').map(x => x.trim())); -// for (const login of logins) { -// const password = process.env[`LOGIN_PASSWORD_${login}`]; -// const permissions = process.env[`LOGIN_PERMISSIONS_${login}`]; -// if (password) { -// res.push({ -// login, -// password, -// permissions, -// }); -// } -// } -// } else if (process.env.OAUTH_PERMISSIONS) { -// const login_permission_keys = Object.keys(process.env).filter(key => _.startsWith(key, 'LOGIN_PERMISSIONS_')); -// for (const permissions_key of login_permission_keys) { -// const login = permissions_key.replace('LOGIN_PERMISSIONS_', ''); -// const permissions = process.env[permissions_key]; -// userPermissions[login] = compilePermissions(permissions); -// } -// } - -// loginsCache = res.length > 0 ? res : null; -// loginsLoaded = true; -// return loginsCache; -// } - -function connectionHasPermission(connection, req) { +function connectionHasPermission(connection, loadedPermissions) { if (!connection) { return true; } if (_.isString(connection)) { - return hasPermission(`connections/${connection}`, req); + return hasPermission(`connections/${connection}`, loadedPermissions); } else { - return hasPermission(`connections/${connection._id}`, req); + return hasPermission(`connections/${connection._id}`, loadedPermissions); } } -function testConnectionPermission(connection, req) { - if (!connectionHasPermission(connection, req)) { - throw new Error('Connection permission not granted'); +async function testConnectionPermission(connection, req, loadedPermissions) { + if (!loadedPermissions) { + loadedPermissions = await loadPermissionsFromRequest(req); + } + if (process.env.STORAGE_DATABASE) { + if (hasPermission(`all-connections`, loadedPermissions)) { + return; + } + const conid = _.isString(connection) ? connection : connection?._id; + const authProvider = getAuthProviderFromReq(req); + if (!req) { + return; + } + if (!await authProvider.checkCurrentConnectionPermission(req, conid)) { + throw new Error('Connection permission not granted'); + } + } else { + if (!connectionHasPermission(connection, loadedPermissions)) { + throw new Error('Connection permission not granted'); + } } } +async function loadDatabasePermissionsFromRequest(req) { + const authProvider = getAuthProviderFromReq(req); + if (!req) { + return null; + } + + const databasePermissions = await authProvider.getCurrentDatabasePermissions(req); + return databasePermissions; +} + +async function loadTablePermissionsFromRequest(req) { + const authProvider = getAuthProviderFromReq(req); + if (!req) { + return null; + } + + const tablePermissions = await authProvider.getCurrentTablePermissions(req); + return tablePermissions; +} + +function matchDatabasePermissionRow(conid, database, permissionRow) { + if (permissionRow.connection_id) { + if (conid != permissionRow.connection_id) { + return false; + } + } + if (permissionRow.database_names_list) { + const items = permissionRow.database_names_list.split('\n'); + if (!items.find(item => item.trim()?.toLowerCase() === database?.toLowerCase())) { + return false; + } + } + if (permissionRow.database_names_regex) { + const regex = new RegExp(permissionRow.database_names_regex, 'i'); + if (!regex.test(database)) { + return false; + } + } + return true; +} + +function matchTablePermissionRow(objectTypeField, schemaName, pureName, permissionRow) { + if (permissionRow.table_names_list) { + const items = permissionRow.table_names_list.split('\n'); + if (!items.find(item => item.trim()?.toLowerCase() === pureName?.toLowerCase())) { + return false; + } + } + if (permissionRow.table_names_regex) { + const regex = new RegExp(permissionRow.table_names_regex, 'i'); + if (!regex.test(pureName)) { + return false; + } + } + if (permissionRow.schema_names_list) { + const items = permissionRow.schema_names_list.split('\n'); + if (!items.find(item => item.trim()?.toLowerCase() === schemaName?.toLowerCase())) { + return false; + } + } + if (permissionRow.schema_names_regex) { + const regex = new RegExp(permissionRow.schema_names_regex, 'i'); + if (!regex.test(schemaName)) { + return false; + } + } + + return true; +} + +const DATABASE_ROLE_ID_NAMES = { + '-1': 'view', + '-2': 'read_content', + '-3': 'write_data', + '-4': 'run_script', + '-5': 'deny', +}; + +function getDatabaseRoleLevelIndex(roleName) { + if (!roleName) { + return 6; + } + if (roleName == 'run_script') { + return 5; + } + if (roleName == 'write_data') { + return 4; + } + if (roleName == 'read_content') { + return 3; + } + if (roleName == 'view') { + return 2; + } + if (roleName == 'deny') { + return 1; + } + return 6; +} + +function getTablePermissionRoleLevelIndex(roleName) { + if (!roleName) { + return 6; + } + if (roleName == 'run_script') { + return 5; + } + if (roleName == 'create_update_delete') { + return 4; + } + if (roleName == 'update_only') { + return 3; + } + if (roleName == 'read') { + return 2; + } + if (roleName == 'deny') { + return 1; + } + return 6; +} + +function getDatabasePermissionRole(conid, database, loadedDatabasePermissions) { + let res = 'deny'; + for (const permissionRow of loadedDatabasePermissions) { + if (!matchDatabasePermissionRow(conid, database, permissionRow)) { + continue; + } + res = DATABASE_ROLE_ID_NAMES[permissionRow.database_permission_role_id]; + } + return res; +} + +const TABLE_ROLE_ID_NAMES = { + '-1': 'read', + '-2': 'update_only', + '-3': 'create_update_delete', + '-4': 'run_script', + '-5': 'deny', +}; + +const TABLE_SCOPE_ID_NAMES = { + '-1': 'all_objects', + '-2': 'tables', + '-3': 'views', + '-4': 'tables_views_collections', + '-5': 'procedures', + '-6': 'functions', + '-7': 'triggers', + '-8': 'sql_objects', + '-9': 'collections', +}; + +function getTablePermissionRole(conid, database, objectTypeField, schemaName, pureName, loadedTablePermissions, databasePermissionRole) { + let res = databasePermissionRole == 'read_content' ? 'read' : + databasePermissionRole == 'write_data' ? 'create_update_delete' : + databasePermissionRole == 'run_script' ? 'run_script' : + 'deny'; + for (const permissionRow of loadedTablePermissions) { + if (!matchDatabasePermissionRow(conid, database, permissionRow)) { + continue; + } + if (!matchTablePermissionRow(objectTypeField, schemaName, pureName, permissionRow)) { + continue; + } + const scope = TABLE_SCOPE_ID_NAMES[permissionRow.table_permission_scope_id]; + switch (scope) { + case 'tables': + if (objectTypeField != 'tables') continue; + break; + case 'views': + if (objectTypeField != 'views') continue; + break; + case 'tables_views_collections': + if (objectTypeField != 'tables' && objectTypeField != 'views' && objectTypeField != 'collections') continue; + break; + case 'procedures': + if (objectTypeField != 'procedures') continue; + break; + case 'functions': + if (objectTypeField != 'functions') continue; + break; + case 'triggers': + if (objectTypeField != 'triggers') continue; + break; + case 'sql_objects': + if (objectTypeField != 'procedures' && objectTypeField != 'functions' && objectTypeField != 'triggers') + continue; + break; + case 'collections': + if (objectTypeField != 'collections') continue; + break; + } + res = TABLE_ROLE_ID_NAMES[permissionRow.table_permission_role_id]; + } + return res; +} + +async function testStandardPermission(permission, req, loadedPermissions) { + if (!loadedPermissions) { + loadedPermissions = await loadPermissionsFromRequest(req); + } + if (!hasPermission(permission, loadedPermissions)) { + throw new Error('Permission not granted'); + } +} + +async function testDatabaseRolePermission(conid, database, requiredRole, req) { + if (!process.env.STORAGE_DATABASE) { + return; + } + const loadedPermissions = await loadPermissionsFromRequest(req); + if (hasPermission(`all-databases`, loadedPermissions)) { + return; + } + const databasePermissions = await loadDatabasePermissionsFromRequest(req); + const role = getDatabasePermissionRole(conid, database, databasePermissions); + const requiredIndex = getDatabaseRoleLevelIndex(requiredRole); + const roleIndex = getDatabaseRoleLevelIndex(role); + if (roleIndex < requiredIndex) { + throw new Error('Permission not granted'); + } +} + + module.exports = { hasPermission, connectionHasPermission, testConnectionPermission, + loadPermissionsFromRequest, + loadDatabasePermissionsFromRequest, + loadTablePermissionsFromRequest, + getDatabasePermissionRole, + getTablePermissionRole, + testStandardPermission, + testDatabaseRolePermission, + getTablePermissionRoleLevelIndex }; diff --git a/packages/tools/src/testPermission.ts b/packages/tools/src/testPermission.ts index 5727d726f..1998a947a 100644 --- a/packages/tools/src/testPermission.ts +++ b/packages/tools/src/testPermission.ts @@ -57,6 +57,12 @@ export function compilePermissions(permissions: string[] | string): CompiledPerm return res; } +export function getPermissionsCacheKey(permissions: string[] | string) { + if (!permissions) return null; + if (_isString(permissions)) return permissions; + return permissions.join('|'); +} + export function testPermission(tested: string, permissions: CompiledPermissions) { let allow = true; @@ -103,9 +109,9 @@ export function getPredefinedPermissions(predefinedRoleName: string) { case 'superadmin': return ['*', '~widgets/*', 'widgets/admin', 'widgets/database', '~all-connections']; case 'logged-user': - return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections']; + return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections', '~run-shell-script']; case 'anonymous-user': - return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections']; + return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections', '~run-shell-script']; default: return null; } diff --git a/packages/types/dbinfo.d.ts b/packages/types/dbinfo.d.ts index ba4d33a4c..787de5c32 100644 --- a/packages/types/dbinfo.d.ts +++ b/packages/types/dbinfo.d.ts @@ -22,7 +22,7 @@ export interface ColumnsConstraintInfo extends ConstraintInfo { columns: ColumnReference[]; } -export interface PrimaryKeyInfo extends ColumnsConstraintInfo {} +export interface PrimaryKeyInfo extends ColumnsConstraintInfo { } export interface ForeignKeyInfo extends ColumnsConstraintInfo { refSchemaName?: string; @@ -39,7 +39,7 @@ export interface IndexInfo extends ColumnsConstraintInfo { filterDefinition?: string; } -export interface UniqueInfo extends ColumnsConstraintInfo {} +export interface UniqueInfo extends ColumnsConstraintInfo { } export interface CheckInfo extends ConstraintInfo { definition: string; @@ -77,6 +77,7 @@ export interface DatabaseObjectInfo extends NamedObjectInfo { hashCode?: string; objectTypeField?: string; objectComment?: string; + tablePermissionRole?: 'read' | 'update_only' | 'create_update_delete' | 'deny'; } export interface SqlObjectInfo extends DatabaseObjectInfo { @@ -134,7 +135,7 @@ export interface CallableObjectInfo extends SqlObjectInfo { parameters?: ParameterInfo[]; } -export interface ProcedureInfo extends CallableObjectInfo {} +export interface ProcedureInfo extends CallableObjectInfo { } export interface FunctionInfo extends CallableObjectInfo { returnType?: string; @@ -145,17 +146,17 @@ export interface TriggerInfo extends SqlObjectInfo { functionName?: string; tableName?: string; triggerTiming?: - | 'BEFORE' - | 'AFTER' - | 'INSTEAD OF' - | 'BEFORE EACH ROW' - | 'INSTEAD OF' - | 'AFTER EACH ROW' - | 'AFTER STATEMENT' - | 'BEFORE STATEMENT' - | 'AFTER EVENT' - | 'BEFORE EVENT' - | null; + | 'BEFORE' + | 'AFTER' + | 'INSTEAD OF' + | 'BEFORE EACH ROW' + | 'INSTEAD OF' + | 'AFTER EACH ROW' + | 'AFTER STATEMENT' + | 'BEFORE STATEMENT' + | 'AFTER EVENT' + | 'BEFORE EVENT' + | null; triggerLevel?: 'ROW' | 'STATEMENT'; eventType?: 'INSERT' | 'UPDATE' | 'DELETE' | 'TRUNCATE'; } diff --git a/packages/web/src/appobj/ArchiveFolderAppObject.svelte b/packages/web/src/appobj/ArchiveFolderAppObject.svelte index d38a9a69e..3a4f66517 100644 --- a/packages/web/src/appobj/ArchiveFolderAppObject.svelte +++ b/packages/web/src/appobj/ArchiveFolderAppObject.svelte @@ -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() && diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte index ffd30484d..c9df74b45 100644 --- a/packages/web/src/appobj/ConnectionAppObject.svelte +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -382,7 +382,8 @@ $extensions, $currentDatabase, $apps, - $openedSingleDatabaseConnections + $openedSingleDatabaseConnections, + data.databasePermissionRole, ), ], diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index e307d6970..9175d8c00 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -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])} diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index 948d2311b..4486dee02 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -703,15 +703,29 @@ } function createMenus(objectTypeField, driver, data): ReturnType { - 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')} diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index c61029a7d..d41a437f7 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -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( { diff --git a/packages/web/src/datagrid/TableDataGrid.svelte b/packages/web/src/datagrid/TableDataGrid.svelte index 2aebada65..9f4488222 100644 --- a/packages/web/src/datagrid/TableDataGrid.svelte +++ b/packages/web/src/datagrid/TableDataGrid.svelte @@ -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 ) diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index ba472760e..238686d4e 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -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', diff --git a/packages/web/src/modals/NewObjectModal.svelte b/packages/web/src/modals/NewObjectModal.svelte index 3995d1129..99d140694 100644 --- a/packages/web/src/modals/NewObjectModal.svelte +++ b/packages/web/src/modals/NewObjectModal.svelte @@ -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', - } + }, ]; @@ -122,7 +124,11 @@
Create new
{#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} getCurrentEditor()?.hasConnection(), + executeAdditionalCondition: () => getCurrentEditor()?.hasConnection() && hasPermission('dbops/query'), copyPaste: true, }); registerCommand({ diff --git a/packages/web/src/tabs/TableDataTab.svelte b/packages/web/src/tabs/TableDataTab.svelte index 3502356fb..0a943d44e 100644 --- a/packages/web/src/tabs/TableDataTab.svelte +++ b/packages/web/src/tabs/TableDataTab.svelte @@ -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', + }); + } } } diff --git a/packages/web/src/utility/hasPermission.ts b/packages/web/src/utility/hasPermission.ts index 0fbff7018..fc824765a 100644 --- a/packages/web/src/utility/hasPermission.ts +++ b/packages/web/src/utility/hasPermission.ts @@ -22,3 +22,8 @@ export function subscribePermissionCompiler() { export function setConfigForPermissions(config) { compiled = compilePermissions(config?.permissions || []); } + +export function isAllowedDatabaseRunScript(databasePermissionRole) { + return !databasePermissionRole || databasePermissionRole == 'run_script'; +} + \ No newline at end of file