Skip to content

Commit

Permalink
feat: Add dynamic master key by allowing to set option masterKey to…
Browse files Browse the repository at this point in the history
… a function (#2655)
  • Loading branch information
dblythy authored Feb 14, 2025
1 parent 3862c32 commit 9025ed0
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 12 deletions.
44 changes: 32 additions & 12 deletions Parse-Dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions Parse-Dashboard/configKeyCache.js
Original file line number Diff line number Diff line change
@@ -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();
1 change: 1 addition & 0 deletions Parse-Dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down

0 comments on commit 9025ed0

Please sign in to comment.