diff --git a/packages/api/.env b/packages/api/.env index 8bea8426d..b38955061 100644 --- a/packages/api/.env +++ b/packages/api/.env @@ -1,4 +1,14 @@ DEVMODE=1 # PERMISSIONS=~widgets/app,~widgets/plugins # DISABLE_SHELL=1 -# HIDE_APP_EDITOR=1 \ No newline at end of file +# HIDE_APP_EDITOR=1 + + +DEVWEB=1 +LOGINS=admin,test + +LOGIN_PASSWORD_admin=admin +LOGIN_PERMISSIONS_admin=* + +LOGIN_PASSWORD_test=test +LOGIN_PERMISSIONS_test=~*, widgets/database diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index 937898a60..acac6038e 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -3,7 +3,7 @@ const os = require('os'); const path = require('path'); const axios = require('axios'); const { datadir } = require('../utility/directories'); -const hasPermission = require('../utility/hasPermission'); +const { hasPermission, getLogins } = require('../utility/hasPermission'); const socket = require('../utility/socket'); const _ = require('lodash'); const AsyncLock = require('async-lock'); @@ -26,8 +26,10 @@ module.exports = { // }, get_meta: true, - async get() { - const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null; + async get(_params, req) { + const logins = getLogins(); + const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null; + const permissions = login ? login.permissions : null; return { runAsPortal: !!connections.portalConnections, @@ -67,8 +69,8 @@ module.exports = { }, updateSettings_meta: true, - async updateSettings(values) { - if (!hasPermission(`settings/change`)) return false; + async updateSettings(values, req) { + if (!hasPermission(`settings/change`, req)) return false; const res = await lock.acquire('update', async () => { const currentValue = await this.getSettings(); diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index 4116bd9e7..6ef6e05f5 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -3,7 +3,7 @@ const fs = require('fs-extra'); const path = require('path'); const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir } = require('../utility/directories'); const getChartExport = require('../utility/getChartExport'); -const hasPermission = require('../utility/hasPermission'); +const { hasPermission } = require('../utility/hasPermission'); const socket = require('../utility/socket'); const scheduler = require('./scheduler'); const getDiagramExport = require('../utility/getDiagramExport'); @@ -23,8 +23,8 @@ function deserialize(format, text) { module.exports = { list_meta: true, - async list({ folder }) { - if (!hasPermission(`files/${folder}/read`)) return []; + async list({ folder }, req) { + if (!hasPermission(`files/${folder}/read`, req)) return []; const dir = path.join(filesdir(), folder); if (!(await fs.exists(dir))) return []; const files = (await fs.readdir(dir)).map(file => ({ folder, file })); @@ -32,11 +32,11 @@ module.exports = { }, listAll_meta: true, - async listAll() { + async listAll(_params, req) { const folders = await fs.readdir(filesdir()); const res = []; for (const folder of folders) { - if (!hasPermission(`files/${folder}/read`)) continue; + if (!hasPermission(`files/${folder}/read`, req)) continue; const dir = path.join(filesdir(), folder); const files = (await fs.readdir(dir)).map(file => ({ folder, file })); res.push(...files); @@ -45,8 +45,8 @@ module.exports = { }, delete_meta: true, - async delete({ folder, file }) { - if (!hasPermission(`files/${folder}/write`)) return false; + async delete({ folder, file }, req) { + if (!hasPermission(`files/${folder}/write`, req)) return false; await fs.unlink(path.join(filesdir(), folder, file)); socket.emitChanged(`files-changed-${folder}`); socket.emitChanged(`all-files-changed`); @@ -54,8 +54,8 @@ module.exports = { }, rename_meta: true, - async rename({ folder, file, newFile }) { - if (!hasPermission(`files/${folder}/write`)) return false; + async rename({ folder, file, newFile }, req) { + if (!hasPermission(`files/${folder}/write`, req)) return false; await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile)); socket.emitChanged(`files-changed-${folder}`); socket.emitChanged(`all-files-changed`); @@ -63,8 +63,8 @@ module.exports = { }, copy_meta: true, - async copy({ folder, file, newFile }) { - if (!hasPermission(`files/${folder}/write`)) return false; + async copy({ folder, file, newFile }, req) { + if (!hasPermission(`files/${folder}/write`, req)) 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`); @@ -72,7 +72,7 @@ module.exports = { }, load_meta: true, - async load({ folder, file, format }) { + async load({ folder, file, format }, req) { if (folder.startsWith('archive:')) { const text = await fs.readFile(path.join(resolveArchiveFolder(folder.substring('archive:'.length)), file), { encoding: 'utf-8', @@ -84,22 +84,22 @@ module.exports = { }); return deserialize(format, text); } else { - if (!hasPermission(`files/${folder}/read`)) return null; + if (!hasPermission(`files/${folder}/read`, req)) return null; const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' }); return deserialize(format, text); } }, save_meta: true, - async save({ folder, file, data, format }) { + async save({ folder, file, data, format }, req) { if (folder.startsWith('archive:')) { - if (!hasPermission(`archive/write`)) return false; + if (!hasPermission(`archive/write`, req)) 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.substring('archive:'.length)}`); return true; } else if (folder.startsWith('app:')) { - if (!hasPermission(`apps/write`)) return false; + if (!hasPermission(`apps/write`, req)) 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}`); @@ -107,7 +107,7 @@ module.exports = { apps.emitChangedDbApp(folder); return true; } else { - if (!hasPermission(`files/${folder}/write`)) return false; + if (!hasPermission(`files/${folder}/write`, req)) return false; const dir = path.join(filesdir(), folder); if (!(await fs.exists(dir))) { await fs.mkdir(dir); @@ -128,8 +128,8 @@ module.exports = { }, favorites_meta: true, - async favorites() { - if (!hasPermission(`files/favorites/read`)) return []; + async favorites(_params, req) { + if (!hasPermission(`files/favorites/read`, req)) return []; const dir = path.join(filesdir(), 'favorites'); if (!(await fs.exists(dir))) return []; const files = await fs.readdir(dir); diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index a83c6e5a4..9caea36a9 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -7,7 +7,7 @@ const socket = require('../utility/socket'); const compareVersions = require('compare-versions'); const requirePlugin = require('../shell/requirePlugin'); const downloadPackage = require('../utility/downloadPackage'); -const hasPermission = require('../utility/hasPermission'); +const { hasPermission } = require('../utility/hasPermission'); const _ = require('lodash'); const packagedPluginsContent = require('../packagedPluginsContent'); @@ -115,8 +115,8 @@ module.exports = { // }, install_meta: true, - async install({ packageName }) { - if (!hasPermission(`plugins/install`)) return; + async install({ packageName }, req) { + if (!hasPermission(`plugins/install`, req)) return; const dir = path.join(pluginsdir(), packageName); // @ts-ignore if (!(await fs.exists(dir))) { @@ -128,8 +128,8 @@ module.exports = { }, uninstall_meta: true, - async uninstall({ packageName }) { - if (!hasPermission(`plugins/install`)) return; + async uninstall({ packageName }, req) { + if (!hasPermission(`plugins/install`, req)) return; const dir = path.join(pluginsdir(), packageName); await fs.rmdir(dir, { recursive: true }); socket.emitChanged(`installed-plugins-changed`); @@ -138,8 +138,8 @@ module.exports = { }, upgrade_meta: true, - async upgrade({ packageName }) { - if (!hasPermission(`plugins/install`)) return; + async upgrade({ packageName }, req) { + if (!hasPermission(`plugins/install`, req)) return; const dir = path.join(pluginsdir(), packageName); // @ts-ignore if (await fs.exists(dir)) { diff --git a/packages/api/src/controllers/scheduler.js b/packages/api/src/controllers/scheduler.js index 0377856ee..b3ff47098 100644 --- a/packages/api/src/controllers/scheduler.js +++ b/packages/api/src/controllers/scheduler.js @@ -3,7 +3,7 @@ const fs = require('fs-extra'); const path = require('path'); const cron = require('node-cron'); const runners = require('./runners'); -const hasPermission = require('../utility/hasPermission'); +const { hasPermission } = require('../utility/hasPermission'); const scheduleRegex = /\s*\/\/\s*@schedule\s+([^\n]+)\n/; @@ -26,8 +26,8 @@ module.exports = { this.tasks.push(task); }, - async reload() { - if (!hasPermission('files/shell/read')) return; + async reload(_params, req) { + if (!hasPermission('files/shell/read', req)) return; const shellDir = path.join(filesdir(), 'shell'); await this.unload(); if (!(await fs.exists(shellDir))) return; diff --git a/packages/api/src/main.js b/packages/api/src/main.js index 275d6c672..63b67ca8c 100644 --- a/packages/api/src/main.js +++ b/packages/api/src/main.js @@ -29,6 +29,8 @@ const queryHistory = require('./controllers/queryHistory'); const { rundir } = require('./utility/directories'); const platformInfo = require('./utility/platformInfo'); const getExpressPath = require('./utility/getExpressPath'); +const { getLogins } = require('./utility/hasPermission'); +const _ = require('lodash'); function start() { // console.log('process.argv', process.argv); @@ -37,12 +39,11 @@ function start() { const server = http.createServer(app); - if (process.env.LOGIN && process.env.PASSWORD) { + const logins = getLogins(); + if (logins) { app.use( basicAuth({ - users: { - [process.env.LOGIN]: process.env.PASSWORD, - }, + users: _.fromPairs(logins.map(x => [x.login, x.password])), challenge: true, realm: 'DbGate Web App', }) @@ -85,15 +86,7 @@ function start() { if (platformInfo.isDocker) { // server static files inside docker container app.use(getExpressPath('/'), express.static('/home/dbgate-docker/public')); - } else { - if (!platformInfo.isNpmDist) { - app.get(getExpressPath('/'), (req, res) => { - res.send('DbGate API'); - }); - } - } - - if (platformInfo.isNpmDist) { + } else if (platformInfo.isNpmDist) { app.use(getExpressPath('/'), express.static(path.join(__dirname, '../../dbgate-web/public'))); getPort({ port: parseInt( @@ -105,7 +98,19 @@ function start() { console.log(`DbGate API listening on port ${port}`); }); }); + } else if (process.env.DEVWEB) { + console.log('__dirname', __dirname); + console.log(path.join(__dirname, '../../web/public/build')); + app.use(getExpressPath('/'), express.static(path.join(__dirname, '../../web/public'))); + + const port = process.env.PORT || 3000; + console.log('DbGate API & web listening on port', port); + server.listen(port); } else { + app.get(getExpressPath('/'), (req, res) => { + res.send('DbGate API'); + }); + const port = process.env.PORT || 3000; console.log('DbGate API listening on port', port); server.listen(port); diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js index 64f3a3a72..54696b5b1 100644 --- a/packages/api/src/utility/hasPermission.js +++ b/packages/api/src/utility/hasPermission.js @@ -1,12 +1,56 @@ const { compilePermissions, testPermission } = require('dbgate-tools'); +const _ = require('lodash'); -let compiled = undefined; +const userPermissions = {}; -function hasPermission(tested) { - if (compiled === undefined) { - compiled = compilePermissions(process.env.PERMISSIONS); +function hasPermission(tested, req) { + const { user } = (req && req.auth) || {}; + const key = user || ''; + const logins = getLogins(); + if (!userPermissions[key] && logins) { + const login = logins.find(x => x.login == user); + userPermissions[key] = compilePermissions(login ? login.permissions : null); } - return testPermission(tested, compiled); + return testPermission(tested, userPermissions[key]); } -module.exports = hasPermission; +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, + }); + } + } + } + + loginsCache = res.length > 0 ? res : null; + loginsLoaded = true; + return loginsCache; +} + +module.exports = { + hasPermission, + getLogins, +}; diff --git a/packages/api/src/utility/useController.js b/packages/api/src/utility/useController.js index ee891b610..3e1b82d4c 100644 --- a/packages/api/src/utility/useController.js +++ b/packages/api/src/utility/useController.js @@ -62,7 +62,7 @@ module.exports = function useController(app, electron, route, controller) { // controller._init_called = true; // } try { - let params = [{ ...req.body, ...req.query }]; + let params = [{ ...req.body, ...req.query }, req]; if (rawParams) params = [req, res]; const data = await controller[key](...params); res.json(data);