diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 5ec6cc26b9..5e3ecd4ae7 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -5,6 +5,7 @@ const packageJson = require('package-json'); const csrf = require('csurf'); const Authentication = require('./Authentication.js'); const fs = require('fs'); +const ConfigKeyCache = require('./configKeyCache.js'); const currentVersionFeatures = require('../package.json').parseDashboardFeatures; @@ -80,11 +81,11 @@ module.exports = function(config, options) { }); // Serve the configuration. - app.get('/parse-dashboard-config.json', function(req, res) { + app.get('/parse-dashboard-config.json', async (req, res) => { const apps = config.apps.map((app) => Object.assign({}, app)); // make a copy const response = { - apps: apps, - newFeaturesInLatestVersion: newFeaturesInLatestVersion, + apps, + newFeaturesInLatestVersion, }; //Based on advice from Doug Wilson here: @@ -119,20 +120,31 @@ module.exports = function(config, options) { return app; }); } - if (successfulAuth) { if (appsUserHasAccess) { - // Restric access to apps defined in user dictionary - // If they didn't supply any app id, user will access all apps - response.apps = response.apps.filter(function (app) { - return appsUserHasAccess.find(appUserHasAccess => { - const isSame = app.appId === appUserHasAccess.appId; - if (isSame && appUserHasAccess.readOnly) { + const processedApps = await Promise.all( + response.apps.map(async (app) => { + const matchingAccess = appsUserHasAccess.find( + (access) => access.appId === app.appId + ); + + if (!matchingAccess) { + return null; + } + + if (matchingAccess.readOnly) { app.masterKey = app.readOnlyMasterKey; } - return isSame; + + if (typeof app.masterKey === 'function') { + app.masterKey = await ConfigKeyCache.get(app.appId, 'masterKey', app.masterKeyTtl, app.masterKey); + } + + return app; }) - }); + ); + + response.apps = processedApps.filter((app) => app !== null); } // They provided correct auth return res.json(response); @@ -147,6 +159,14 @@ module.exports = function(config, options) { //(ie. didn't supply usernames and passwords) if (requestIsLocal || options.dev) { //Allow no-auth access on localhost only, if they have configured the dashboard to not need auth + await Promise.all( + response.apps.map(async (app) => { + if (typeof app.masterKey === 'function') { + app.masterKey = await ConfigKeyCache.get(app.appId, 'masterKey', app.masterKeyTtl, app.masterKey); + } + }) + ); + return res.json(response); } //We shouldn't get here. Fail closed. diff --git a/Parse-Dashboard/configKeyCache.js b/Parse-Dashboard/configKeyCache.js new file mode 100644 index 0000000000..3b8ad9b31f --- /dev/null +++ b/Parse-Dashboard/configKeyCache.js @@ -0,0 +1,22 @@ +class KeyCache { + constructor() { + this.cache = {}; + } + + async get(appId, key, ttl, callback) { + key = `${appId}:${key}`; + const cached = this.cache[key]; + if (cached && cached.expiry > Date.now()) { + return cached.value; + } + + const value = await Promise.resolve(callback()); + this.cache[key] = { + value, + expiry: Date.now() + ttl, + }; + return value; + } +} + +module.exports = new KeyCache(); diff --git a/Parse-Dashboard/index.js b/Parse-Dashboard/index.js index 6217df3e95..f472ec0844 100644 --- a/Parse-Dashboard/index.js +++ b/Parse-Dashboard/index.js @@ -13,6 +13,7 @@ const startServer = require('./server'); const program = require('commander'); program.option('--appId [appId]', 'the app Id of the app you would like to manage.'); program.option('--masterKey [masterKey]', 'the master key of the app you would like to manage.'); +program.option('--masterKeyTtl [masterKeyTtl]', 'the master key ttl of the app you would like to manage.'); program.option('--serverURL [serverURL]', 'the server url of the app you would like to manage.'); program.option('--graphQLServerURL [graphQLServerURL]', 'the GraphQL server url of the app you would like to manage.'); program.option('--dev', 'Enable development mode. This will disable authentication and allow non HTTPS connections. DO NOT ENABLE IN PRODUCTION SERVERS'); diff --git a/README.md b/README.md index 08a31a0de2..01b3d47af8 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,11 @@ Parse Dashboard is continuously tested with the most recent releases of Node.js | Parameter | Type | Optional | Default | Example | Description | |----------------------------------------|---------------------|----------|---------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------| | `apps` | Array<Object> | no | - | `[{ ... }, { ... }]` | The apps that are configured for the dashboard. | +| `apps.appId` | String | yes | - | `"myAppId"` | The Application ID for your Parse Server instance. | +| `apps.masterKey` | String \| Function | yes | - | `"exampleMasterKey"`, `() => "exampleMasterKey"` | The master key for full access to Parse Server. It can be provided directly as a String or as a Function returning a String. | +| `apps.masterKeyTtl` | Number | no | - | `3600` | Time-to-live (TTL) for the master key in seconds. This defines how long the master key is cached before the `masterKey` function is re-triggered. | +| `apps.serverURL` | String | yes | - | `"http://localhost:1337/parse"` | The URL where your Parse Server is running. | +| `apps.appName` | String | no | - | `"MyApp"` | The display name of the app in the dashboard. | | `infoPanel` | Array<Object> | yes | - | `[{ ... }, { ... }]` | The [info panel](#info-panel) configuration. | | `infoPanel[*].title` | String | no | - | `User Details` | The panel title. | | `infoPanel[*].classes` | Array<String> | no | - | `["_User"]` | The classes for which the info panel should be displayed. |