diff --git a/packages/api/src/auth/authCommon.js b/packages/api/src/auth/authCommon.js index 824b9baa8..9863dfefe 100644 --- a/packages/api/src/auth/authCommon.js +++ b/packages/api/src/auth/authCommon.js @@ -10,7 +10,13 @@ function getTokenSecret() { return tokenSecret; } +function getStaticTokenSecret() { + // TODO static not fixed + return '14813c43-a91b-4ad1-9dcd-a81bd7dbb05f'; +} + module.exports = { getTokenLifetime, getTokenSecret, + getStaticTokenSecret, }; diff --git a/packages/api/src/controllers/apps.js b/packages/api/src/controllers/apps.js index ac2f3aafb..9425013e4 100644 --- a/packages/api/src/controllers/apps.js +++ b/packages/api/src/controllers/apps.js @@ -21,7 +21,7 @@ module.exports = { const filePermissions = await loadFilePermissionsFromRequest(req); for (const file of await fs.readdir(dir)) { - if (!hasPermission(`all-files`, loadedPermissions)) { + if (!hasPermission(`all-disk-files`, loadedPermissions)) { const role = getFilePermissionRole('apps', file, filePermissions); if (role == 'deny') continue; } diff --git a/packages/api/src/controllers/sessions.js b/packages/api/src/controllers/sessions.js index 5d5f9bc81..14c8a7bed 100644 --- a/packages/api/src/controllers/sessions.js +++ b/packages/api/src/controllers/sessions.js @@ -8,11 +8,13 @@ const path = require('path'); const { handleProcessCommunication } = require('../utility/processComm'); const processArgs = require('../utility/processArgs'); const { appdir } = require('../utility/directories'); -const { getLogger, extractErrorLogData } = require('dbgate-tools'); +const { getLogger, extractErrorLogData, removeSqlFrontMatter } = require('dbgate-tools'); const pipeForkLogs = require('../utility/pipeForkLogs'); const config = require('./config'); const { sendToAuditLog } = require('../utility/auditlog'); const { testStandardPermission, testDatabaseRolePermission } = require('../utility/hasPermission'); +const { getStaticTokenSecret } = require('../auth/authCommon'); +const jwt = require('jsonwebtoken'); const logger = getLogger('sessions'); @@ -95,7 +97,7 @@ module.exports = { socket.emit(`session-initialize-file-${jslid}`); }, - handle_ping() { }, + handle_ping() {}, create_meta: true, async create({ conid, database }) { @@ -149,12 +151,23 @@ module.exports = { executeQuery_meta: true, async executeQuery({ sesid, sql, autoCommit, autoDetectCharts, limitRows, frontMatter }, req) { - await testStandardPermission('dbops/query', req); + let useTokenIsOk = false; + if (frontMatter?.useToken) { + const decoded = jwt.verify(frontMatter.useToken, getStaticTokenSecret()); + if (decoded?.['contentHash'] == crypto.createHash('md5').update(removeSqlFrontMatter(sql)).digest('hex')) { + useTokenIsOk = true; + } + } + if (!useTokenIsOk) { + 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); + if (!useTokenIsOk) { + await testDatabaseRolePermission(session.conid, session.database, 'run_script', req); + } sendToAuditLog(req, { category: 'dbop', diff --git a/packages/api/src/main.js b/packages/api/src/main.js index d59c8b3df..314c5c899 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -29,6 +29,8 @@ const files = require('./controllers/files'); const scheduler = require('./controllers/scheduler'); const queryHistory = require('./controllers/queryHistory'); const cloud = require('./controllers/cloud'); +const teamFiles = require('./controllers/teamFiles'); + const onFinished = require('on-finished'); const processArgs = require('./utility/processArgs'); @@ -264,6 +266,7 @@ function useAllControllers(app, electron) { useController(app, electron, '/apps', apps); useController(app, electron, '/auth', auth); useController(app, electron, '/cloud', cloud); + useController(app, electron, '/team-files', teamFiles); } function setElectronSender(electronSender) { diff --git a/packages/api/src/storageModel.js b/packages/api/src/storageModel.js index 3c8ee800f..54a83a188 100644 --- a/packages/api/src/storageModel.js +++ b/packages/api/src/storageModel.js @@ -784,49 +784,6 @@ 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": "role_connections", "columns": [ @@ -1243,6 +1200,129 @@ module.exports = { ] } }, + { + "pureName": "role_team_files", + "columns": [ + { + "pureName": "role_team_files", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "role_team_files", + "columnName": "role_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "role_team_files", + "columnName": "team_file_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "role_team_files", + "columnName": "allow_read", + "dataType": "int", + "notNull": false + }, + { + "pureName": "role_team_files", + "columnName": "allow_write", + "dataType": "int", + "notNull": false + }, + { + "pureName": "role_team_files", + "columnName": "allow_use", + "dataType": "int", + "notNull": false + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_role_team_files_role_id", + "pureName": "role_team_files", + "refTableName": "roles", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "role_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_role_team_files_team_file_id", + "pureName": "role_team_files", + "refTableName": "team_files", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "team_file_id", + "refColumnName": "id" + } + ] + } + ], + "primaryKey": { + "pureName": "role_team_files", + "constraintType": "primaryKey", + "constraintName": "PK_role_team_files", + "columns": [ + { + "columnName": "id" + } + ] + } + }, + { + "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": [ @@ -1362,39 +1442,111 @@ module.exports = { ] }, { - "pureName": "users", + "pureName": "team_file_types", "columns": [ { - "pureName": "users", + "pureName": "team_file_types", + "columnName": "id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "team_file_types", + "columnName": "name", + "dataType": "varchar(250)", + "notNull": true + } + ], + "foreignKeys": [], + "primaryKey": { + "pureName": "team_file_types", + "constraintType": "primaryKey", + "constraintName": "PK_team_file_types", + "columns": [ + { + "columnName": "id" + } + ] + }, + "preloadedRows": [ + { + "id": -1, + "name": "sql" + } + ] + }, + { + "pureName": "team_files", + "columns": [ + { + "pureName": "team_files", "columnName": "id", "dataType": "int", "autoIncrement": true, "notNull": true }, { - "pureName": "users", - "columnName": "login", + "pureName": "team_files", + "columnName": "file_name", "dataType": "varchar(250)", "notNull": false }, { - "pureName": "users", - "columnName": "password", - "dataType": "varchar(250)", + "pureName": "team_files", + "columnName": "file_content", + "dataType": "text", "notNull": false }, { - "pureName": "users", - "columnName": "email", - "dataType": "varchar(250)", + "pureName": "team_files", + "columnName": "file_type_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "team_files", + "columnName": "owner_user_id", + "dataType": "int", + "notNull": false + }, + { + "pureName": "team_files", + "columnName": "metadata", + "dataType": "varchar(1000)", "notNull": false } ], - "foreignKeys": [], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_team_files_file_type_id", + "pureName": "team_files", + "refTableName": "team_file_types", + "columns": [ + { + "columnName": "file_type_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_team_files_owner_user_id", + "pureName": "team_files", + "refTableName": "users", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "owner_user_id", + "refColumnName": "id" + } + ] + } + ], "primaryKey": { - "pureName": "users", + "pureName": "team_files", "constraintType": "primaryKey", - "constraintName": "PK_users", + "constraintName": "PK_team_files", "columns": [ { "columnName": "id" @@ -1879,6 +2031,127 @@ module.exports = { } ] } + }, + { + "pureName": "user_team_files", + "columns": [ + { + "pureName": "user_team_files", + "columnName": "id", + "dataType": "int", + "autoIncrement": true, + "notNull": true + }, + { + "pureName": "user_team_files", + "columnName": "user_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "user_team_files", + "columnName": "team_file_id", + "dataType": "int", + "notNull": true + }, + { + "pureName": "user_team_files", + "columnName": "allow_read", + "dataType": "int", + "notNull": false + }, + { + "pureName": "user_team_files", + "columnName": "allow_write", + "dataType": "int", + "notNull": false + }, + { + "pureName": "user_team_files", + "columnName": "allow_use", + "dataType": "int", + "notNull": false + } + ], + "foreignKeys": [ + { + "constraintType": "foreignKey", + "constraintName": "FK_user_team_files_user_id", + "pureName": "user_team_files", + "refTableName": "users", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "user_id", + "refColumnName": "id" + } + ] + }, + { + "constraintType": "foreignKey", + "constraintName": "FK_user_team_files_team_file_id", + "pureName": "user_team_files", + "refTableName": "team_files", + "deleteAction": "CASCADE", + "columns": [ + { + "columnName": "team_file_id", + "refColumnName": "id" + } + ] + } + ], + "primaryKey": { + "pureName": "user_team_files", + "constraintType": "primaryKey", + "constraintName": "PK_user_team_files", + "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 2e9902c2f..f964593b1 100644 --- a/packages/api/src/utility/hasPermission.js +++ b/packages/api/src/utility/hasPermission.js @@ -327,7 +327,7 @@ async function testStandardPermission(permission, req, loadedPermissions) { loadedPermissions = await loadPermissionsFromRequest(req); } if (!hasPermission(permission, loadedPermissions)) { - throw new Error('DBGM-00265 Permission not granted'); + throw new Error(`DBGM-00265 Permission ${permission} not granted`); } } @@ -344,7 +344,7 @@ async function testDatabaseRolePermission(conid, database, requiredRole, req) { const requiredIndex = getDatabaseRoleLevelIndex(requiredRole); const roleIndex = getDatabaseRoleLevelIndex(role); if (roleIndex < requiredIndex) { - throw new Error('DBGM-00266 Permission not granted'); + throw new Error(`DBGM-00266 Permission ${requiredRole} not granted`); } } diff --git a/packages/tools/src/testPermission.ts b/packages/tools/src/testPermission.ts index 1998a947a..9bc57cd53 100644 --- a/packages/tools/src/testPermission.ts +++ b/packages/tools/src/testPermission.ts @@ -107,11 +107,27 @@ export function testSubPermission( export function getPredefinedPermissions(predefinedRoleName: string) { switch (predefinedRoleName) { case 'superadmin': - return ['*', '~widgets/*', 'widgets/admin', 'widgets/database', '~all-connections']; + return ['*', '~widgets/*', 'widgets/admin', 'widgets/database', '~all-connections', '~all-team-files/*']; case 'logged-user': - return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections', '~run-shell-script']; + return [ + '*', + '~widgets/admin', + '~admin/*', + '~internal-storage', + '~all-connections', + '~run-shell-script', + '~all-team-files/*', + ]; case 'anonymous-user': - return ['*', '~widgets/admin', '~admin/*', '~internal-storage', '~all-connections', '~run-shell-script']; + return [ + '*', + '~widgets/admin', + '~admin/*', + '~internal-storage', + '~all-connections', + '~run-shell-script', + '~all-team-files/*', + ]; default: return null; } diff --git a/packages/web/src/appobj/SavedFileAppObject.svelte b/packages/web/src/appobj/SavedFileAppObject.svelte index 01060e41a..89776c195 100644 --- a/packages/web/src/appobj/SavedFileAppObject.svelte +++ b/packages/web/src/appobj/SavedFileAppObject.svelte @@ -192,6 +192,7 @@ import { isProApp } from '../utility/proTools'; import { saveFileToDisk } from '../utility/exportFileTools'; import { getConnectionInfo } from '../utility/metadataLoaders'; + import { showSnackbarError } from '../utility/snackbar'; export let data; @@ -214,11 +215,20 @@ function createMenu() { return [ handler?.tabComponent && { text: 'Open', onClick: openTab }, - hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename }, - hasPermission(`files/${data.folder}/write`) && { text: 'Create copy', onClick: handleCopy }, - hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete }, + + !data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename }, + !data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: 'Create copy', onClick: handleCopy }, + !data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete }, + + data.teamFileId && data.allowWrite && { text: 'Rename', onClick: handleRename }, + data.teamFileId && + data.allowRead && + hasPermission('all-team-files/create') && { text: 'Create copy', onClick: handleCopy }, + data.teamFileId && data.allowWrite && { text: 'Delete', onClick: handleDelete }, + folder == 'markdown' && { text: 'Show page', onClick: showMarkdownPage }, - { text: 'Download', onClick: handleDownload }, + !data.teamFileId && { text: 'Download', onClick: handleDownload }, + data.teamFileId && data.allowRead && { text: 'Download', onClick: handleDownload }, ]; } @@ -226,7 +236,9 @@ showModal(ConfirmModal, { message: `Really delete file ${data.file}?`, onConfirm: () => { - if (data.folid && data.cntid) { + if (data.teamFileId) { + apiCall('team-files/delete', { teamFileId: data.teamFileId }); + } else if (data.folid && data.cntid) { apiCall('cloud/delete-content', { folid: data.folid, cntid: data.cntid, @@ -244,7 +256,9 @@ label: 'New file name', header: 'Rename file', onConfirm: newFile => { - if (data.folid && data.cntid) { + if (data.teamFileId) { + apiCall('team-files/update', { teamFileId: data.teamFileId, name: newFile }); + } else if (data.folid && data.cntid) { apiCall('cloud/rename-content', { folid: data.folid, cntid: data.cntid, @@ -263,7 +277,9 @@ label: 'New file name', header: 'Copy file', onConfirm: newFile => { - if (data.folid && data.cntid) { + if (data.teamFileId) { + apiCall('team-files/copy', { teamFileId: data.teamFileId, newName: newFile }); + } else if (data.folid && data.cntid) { apiCall('cloud/copy-file', { folid: data.folid, cntid: data.cntid, @@ -279,7 +295,12 @@ const handleDownload = () => { saveFileToDisk( async filePath => { - if (data.folid && data.cntid) { + if (data.teamFileId) { + await apiCall('team-files/export-file', { + teamFileId: data.teamFileId, + filePath, + }); + } else if (data.folid && data.cntid) { await apiCall('cloud/export-file', { folid: data.folid, cntid: data.cntid, @@ -299,7 +320,23 @@ async function openTab() { let dataContent; - if (data.folid && data.cntid) { + if (data.teamFileId) { + if (data?.metadata?.autoExecute) { + if (!data.allowUse) { + showSnackbarError('You do not have permission to use this team file'); + return; + } + } else { + if (!data.allowRead) { + showSnackbarError('You do not have permission to read this team file'); + return; + } + } + const resp = await apiCall('team-files/get-content', { + teamFileId: data.teamFileId, + }); + dataContent = resp.content; + } else if (data.folid && data.cntid) { const resp = await apiCall('cloud/get-content', { folid: data.folid, cntid: data.cntid, @@ -324,6 +361,11 @@ tooltip = `${getConnectionLabel(connection)}\n${database}`; } + if (data?.metadata?.connectionId) { + connProps.conid = data.metadata.connectionId; + connProps.database = data.metadata.databaseName; + } + openNewTab( { title: data.file, @@ -336,6 +378,8 @@ savedFormat: handler.format, savedCloudFolderId: data.folid, savedCloudContentId: data.cntid, + savedTeamFileId: data.teamFileId, + hideEditor: data.teamFileId && data?.metadata?.autoExecute && !data.allowRead, ...connProps, }, }, diff --git a/packages/web/src/elements/VerticalSplitter.svelte b/packages/web/src/elements/VerticalSplitter.svelte index 74d81302c..faeea13c0 100644 --- a/packages/web/src/elements/VerticalSplitter.svelte +++ b/packages/web/src/elements/VerticalSplitter.svelte @@ -7,6 +7,7 @@ export let isSplitter = true; export let initialValue = undefined; + export let hideFirst = false; export let allowCollapseChild1 = false; export let allowCollapseChild2 = false; @@ -22,28 +23,32 @@