diff --git a/packages/api/.covid-env b/packages/api/.covid-env index d8b4c69f1..b7606fc91 100644 --- a/packages/api/.covid-env +++ b/packages/api/.covid-env @@ -8,3 +8,5 @@ PORT_mysql=3326 ENGINE_mysql=mysql@dbgate-plugin-mysql SINGLE_CONNECTION=mysql SINGLE_DATABASE=covid + +PERMISSIONS=files/charts/read diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index c288d870f..8110677ba 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -11,17 +11,21 @@ module.exports = { })) : null; const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : []; + const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null; + const singleDatabase = + process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE + ? { + conid: process.env.SINGLE_CONNECTION, + database: process.env.SINGLE_DATABASE, + } + : null; + return { runAsPortal: !!process.env.CONNECTIONS, toolbar, startupPages, - singleDatabase: - process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE - ? { - conid: process.env.SINGLE_CONNECTION, - database: process.env.SINGLE_DATABASE, - } - : null, + singleDatabase, + permissions, }; }, }; diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index e87bcde3e..2e2752e1f 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -1,6 +1,7 @@ const fs = require('fs-extra'); const path = require('path'); const { filesdir } = require('../utility/directories'); +const hasPermission = require('../utility/hasPermission'); const socket = require('../utility/socket'); const scheduler = require('./scheduler'); @@ -19,6 +20,7 @@ function deserialize(format, text) { module.exports = { list_meta: 'get', async list({ folder }) { + if (!hasPermission(`files/${folder}/read`)) return []; const dir = path.join(filesdir(), folder); if (!(await fs.exists(dir))) return []; const files = (await fs.readdir(dir)).map((file) => ({ folder, file })); @@ -27,24 +29,28 @@ module.exports = { delete_meta: 'post', async delete({ folder, file }) { + if (!hasPermission(`files/${folder}/write`)) return; await fs.unlink(path.join(filesdir(), folder, file)); socket.emitChanged(`files-changed-${folder}`); }, rename_meta: 'post', async rename({ folder, file, newFile }) { + if (!hasPermission(`files/${folder}/write`)) return; await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile)); socket.emitChanged(`files-changed-${folder}`); }, load_meta: 'post', async load({ folder, file, format }) { + if (!hasPermission(`files/${folder}/read`)) return null; const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' }); return deserialize(format, text); }, save_meta: 'post', async save({ folder, file, data, format }) { + if (!hasPermission(`files/${folder}/write`)) return; const dir = path.join(filesdir(), folder); if (!(await fs.exists(dir))) { await fs.mkdir(dir); diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index ab538b8fe..efb381f0a 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -5,6 +5,7 @@ const { pluginsdir, datadir } = require('../utility/directories'); const socket = require('../utility/socket'); const requirePlugin = require('../shell/requirePlugin'); const downloadPackage = require('../utility/downloadPackage'); +const hasPermission = require('../utility/hasPermission'); // async function loadPackageInfo(dir) { // const readmeFile = path.join(dir, 'README.md'); @@ -106,6 +107,7 @@ module.exports = { install_meta: 'post', async install({ packageName }) { + if (!hasPermission(`plugins/install`)) return; const dir = path.join(pluginsdir(), packageName); if (!(await fs.exists(dir))) { await downloadPackage(packageName, dir); @@ -115,6 +117,7 @@ module.exports = { uninstall_meta: 'post', async uninstall({ packageName }) { + if (!hasPermission(`plugins/install`)) return; const dir = path.join(pluginsdir(), packageName); await fs.rmdir(dir, { recursive: true }); socket.emitChanged(`installed-plugins-changed`); diff --git a/packages/api/src/controllers/scheduler.js b/packages/api/src/controllers/scheduler.js index 3a68e9922..5b9130589 100644 --- a/packages/api/src/controllers/scheduler.js +++ b/packages/api/src/controllers/scheduler.js @@ -3,6 +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 scheduleRegex = /\s*\/\/\s*@schedule\s+([^\n]+)\n/; @@ -26,6 +27,7 @@ module.exports = { }, async reload() { + if (!hasPermission('files/shell/read')) return; const shellDir = path.join(filesdir(), 'shell'); await this.unload(); if (!(await fs.exists(shellDir))) return; diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js new file mode 100644 index 000000000..64f3a3a72 --- /dev/null +++ b/packages/api/src/utility/hasPermission.js @@ -0,0 +1,12 @@ +const { compilePermissions, testPermission } = require('dbgate-tools'); + +let compiled = undefined; + +function hasPermission(tested) { + if (compiled === undefined) { + compiled = compilePermissions(process.env.PERMISSIONS); + } + return testPermission(tested, compiled); +} + +module.exports = hasPermission; diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 857fa0aec..e02763b28 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -6,3 +6,4 @@ export * from './createBulkInsertStreamBase'; export * from './DatabaseAnalyser'; export * from './driverBase'; export * from './SqlDumper'; +export * from './testPermission'; diff --git a/packages/tools/src/testPermission.ts b/packages/tools/src/testPermission.ts new file mode 100644 index 000000000..6e229cb47 --- /dev/null +++ b/packages/tools/src/testPermission.ts @@ -0,0 +1,16 @@ +import _escapeRegExp from 'lodash/escapeRegExp'; +import _isString from 'lodash/isString'; + +export function compilePermissions(permissions: string[] | string) { + if (!permissions) return null; + if (_isString(permissions)) permissions = permissions.split(','); + return permissions.map((x) => new RegExp('^' + _escapeRegExp(x).replace(/\\\*/g, '.*') + '$')); +} + +export function testPermission(tested: string, permissions: RegExp[]) { + if (!permissions) return true; + for (const permission of permissions) { + if (tested.match(permission)) return true; + } + return false; +} diff --git a/packages/web/src/appobj/SavedFileAppObject.js b/packages/web/src/appobj/SavedFileAppObject.js index 3ee42ecdf..db2428fa2 100644 --- a/packages/web/src/appobj/SavedFileAppObject.js +++ b/packages/web/src/appobj/SavedFileAppObject.js @@ -10,8 +10,10 @@ import ScriptWriter from '../impexp/ScriptWriter'; import { extractPackageName } from 'dbgate-tools'; import useShowModal from '../modals/showModal'; import InputTextModal from '../modals/InputTextModal'; +import useHasPermission from '../utility/useHasPermission'; function Menu({ data, menuExt = null }) { + const hasPermission = useHasPermission(); const showModal = useShowModal(); const handleDelete = () => { axios.post('files/delete', data); @@ -31,8 +33,12 @@ function Menu({ data, menuExt = null }) { }; return ( <> - Delete - Rename + {hasPermission(`files/${data.folder}/write`) && ( + Delete + )} + {hasPermission(`files/${data.folder}/write`) && ( + Rename + )} {menuExt} ); diff --git a/packages/web/src/charts/ChartToolbar.js b/packages/web/src/charts/ChartToolbar.js index 617834c7e..df7b9b109 100644 --- a/packages/web/src/charts/ChartToolbar.js +++ b/packages/web/src/charts/ChartToolbar.js @@ -1,12 +1,17 @@ import React from 'react'; +import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; export default function ChartToolbar({ save }) { + const hasPermission = useHasPermission(); + return ( <> - - Save - + {hasPermission('files/charts/write') && ( + + Save + + )} ); } diff --git a/packages/web/src/query/QueryToolbar.js b/packages/web/src/query/QueryToolbar.js index 33a5e46c0..99066b581 100644 --- a/packages/web/src/query/QueryToolbar.js +++ b/packages/web/src/query/QueryToolbar.js @@ -1,7 +1,9 @@ import React from 'react'; +import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; export default function QueryToolbar({ execute, cancel, isDatabaseDefined, busy, save, format, isConnected, kill }) { + const hasPermission = useHasPermission(); return ( <> @@ -13,9 +15,11 @@ export default function QueryToolbar({ execute, cancel, isDatabaseDefined, busy, Kill - - Save - + {hasPermission('files/sql/write') && ( + + Save + + )} Format diff --git a/packages/web/src/query/ShellToolbar.js b/packages/web/src/query/ShellToolbar.js index 1d4c1fbc1..f141a8bd7 100644 --- a/packages/web/src/query/ShellToolbar.js +++ b/packages/web/src/query/ShellToolbar.js @@ -1,7 +1,9 @@ import React from 'react'; +import useHasPermission from '../utility/useHasPermission'; import ToolbarButton from '../widgets/ToolbarButton'; export default function ShellToolbar({ execute, cancel, busy, edit, save, editAvailable }) { + const hasPermission = useHasPermission(); return ( <> @@ -13,9 +15,11 @@ export default function ShellToolbar({ execute, cancel, busy, edit, save, editAv Show wizard - - Save - + {hasPermission('files/shell/write') && ( + + Save + + )} ); } diff --git a/packages/web/src/tabs/PluginTab.js b/packages/web/src/tabs/PluginTab.js index 24d2cbbf5..87bfa1171 100644 --- a/packages/web/src/tabs/PluginTab.js +++ b/packages/web/src/tabs/PluginTab.js @@ -9,6 +9,7 @@ import { extractPluginIcon, extractPluginAuthor } from '../plugins/manifestExtra import FormStyledButton from '../widgets/FormStyledButton'; import axios from '../utility/axios'; import { useInstalledPlugins } from '../utility/metadataLoaders'; +import useHasPermission from '../utility/useHasPermission'; const WhitePage = styled.div` position: absolute; @@ -56,6 +57,7 @@ function Delimiter() { } function PluginTabCore({ packageName }) { + const hasPermission = useHasPermission(); const theme = useTheme(); const installed = useInstalledPlugins(); const info = useFetch({ @@ -98,10 +100,10 @@ function PluginTabCore({ packageName }) { {manifest.version && manifest.version} - {!installed.find((x) => x.name == packageName) && ( + {hasPermission('plugins/install') && !installed.find((x) => x.name == packageName) && ( )} - {!!installed.find((x) => x.name == packageName) && ( + {hasPermission('plugins/install') && !!installed.find((x) => x.name == packageName) && ( )} diff --git a/packages/web/src/utility/useHasPermission.js b/packages/web/src/utility/useHasPermission.js new file mode 100644 index 000000000..b02f605b1 --- /dev/null +++ b/packages/web/src/utility/useHasPermission.js @@ -0,0 +1,10 @@ +import React from 'react'; +import { useConfig } from './metadataLoaders'; +import { compilePermissions, testPermission } from 'dbgate-tools'; + +export default function useHasPermission() { + const config = useConfig(); + const compiled = React.useMemo(() => compilePermissions(config.permissions), [config]); + const hasPermission = (tested) => testPermission(tested, compiled); + return hasPermission; +} diff --git a/packages/web/src/widgets/FilesWidget.js b/packages/web/src/widgets/FilesWidget.js index 1160f5a8c..00c15b212 100644 --- a/packages/web/src/widgets/FilesWidget.js +++ b/packages/web/src/widgets/FilesWidget.js @@ -8,6 +8,7 @@ import { WidgetsInnerContainer } from './WidgetStyles'; import { SavedSqlFileAppObject, SavedShellFileAppObject, SavedChartFileAppObject } from '../appobj/SavedFileAppObject'; import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar'; import { useFiles } from '../utility/metadataLoaders'; +import useHasPermission from '../utility/useHasPermission'; function ClosedTabsList() { const tabs = useOpenedTabs(); @@ -64,20 +65,27 @@ function SavedChartFilesList() { } export default function FilesWidget() { + const hasPermission = useHasPermission(); return ( - - - - - - - - - + {hasPermission('files/sql/read') && ( + + + + )} + {hasPermission('files/shell/read') && ( + + + + )} + {hasPermission('files/charts/read') && ( + + + + )} ); }