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

@@ -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

View File

@@ -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;
}

View File

@@ -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(),
{

View File

@@ -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');
}

View File

@@ -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 });
},

View File

@@ -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');
@@ -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,
@@ -267,8 +267,7 @@ module.exports = {
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
sessionParam: `${conid}::${database}::${select?.from?.name?.schemaName || '0'}::${select?.from?.name?.pureName
}`,
sessionGroup: auditLogSessionGroup,
message: `Loaded table data from ${select?.from?.name?.pureName}`,
@@ -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,
@@ -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',
@@ -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 });

View File

@@ -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;
},

View File

@@ -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)) {

View File

@@ -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') {

View File

@@ -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;

View File

@@ -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');
@@ -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;

View File

@@ -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');
@@ -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',

View File

@@ -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,

View File

@@ -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": [],

View File

@@ -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)) {
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
};

View File

@@ -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;
}

View File

@@ -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 {

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,7 +413,8 @@ await dbgateApi.executeQuery(${JSON.stringify(
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
return [
hasPermission(`dbops/query`) && {
hasPermission(`dbops/query`) &&
isAllowedDatabaseRunScript(databasePermissionRole) && {
onClick: handleNewQuery,
text: _t('database.newQuery', { defaultMessage: 'New query' }),
isNewQuery: true,
@@ -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,7 +703,15 @@
}
function createMenus(objectTypeField, driver, data): ReturnType<typeof createMenusCore> {
return createMenusCore(objectTypeField, driver, data).filter(x => {
const coreMenus = createMenusCore(objectTypeField, driver, data);
const filteredSumenus = coreMenus.map(item => {
if (!item.submenu) {
return item;
}
return {
...item,
submenu: item.submenu.filter(x => {
if (x.scriptTemplate) {
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
}
@@ -711,7 +719,13 @@
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,9 +156,25 @@
}
}
export function save() {
export async function save() {
const driver = findEngineDriver($connection, $extensions);
const tablePermissionRole = (await getTableInfo({ conid, database, schemaName, pureName }))?.tablePermissionRole;
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)
);
@@ -182,6 +198,7 @@
});
}
}
}
export function canSave() {
return changeSetContainsChanges($changeSetStore?.value);

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';
}