Skip to content

Commit 9025ed0

Browse files
authored
feat: Add dynamic master key by allowing to set option masterKey to a function (#2655)
1 parent 3862c32 commit 9025ed0

File tree

4 files changed

+60
-12
lines changed

4 files changed

+60
-12
lines changed

Parse-Dashboard/app.js

+32-12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const packageJson = require('package-json');
55
const csrf = require('csurf');
66
const Authentication = require('./Authentication.js');
77
const fs = require('fs');
8+
const ConfigKeyCache = require('./configKeyCache.js');
89

910
const currentVersionFeatures = require('../package.json').parseDashboardFeatures;
1011

@@ -80,11 +81,11 @@ module.exports = function(config, options) {
8081
});
8182

8283
// Serve the configuration.
83-
app.get('/parse-dashboard-config.json', function(req, res) {
84+
app.get('/parse-dashboard-config.json', async (req, res) => {
8485
const apps = config.apps.map((app) => Object.assign({}, app)); // make a copy
8586
const response = {
86-
apps: apps,
87-
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
87+
apps,
88+
newFeaturesInLatestVersion,
8889
};
8990

9091
//Based on advice from Doug Wilson here:
@@ -119,20 +120,31 @@ module.exports = function(config, options) {
119120
return app;
120121
});
121122
}
122-
123123
if (successfulAuth) {
124124
if (appsUserHasAccess) {
125-
// Restric access to apps defined in user dictionary
126-
// If they didn't supply any app id, user will access all apps
127-
response.apps = response.apps.filter(function (app) {
128-
return appsUserHasAccess.find(appUserHasAccess => {
129-
const isSame = app.appId === appUserHasAccess.appId;
130-
if (isSame && appUserHasAccess.readOnly) {
125+
const processedApps = await Promise.all(
126+
response.apps.map(async (app) => {
127+
const matchingAccess = appsUserHasAccess.find(
128+
(access) => access.appId === app.appId
129+
);
130+
131+
if (!matchingAccess) {
132+
return null;
133+
}
134+
135+
if (matchingAccess.readOnly) {
131136
app.masterKey = app.readOnlyMasterKey;
132137
}
133-
return isSame;
138+
139+
if (typeof app.masterKey === 'function') {
140+
app.masterKey = await ConfigKeyCache.get(app.appId, 'masterKey', app.masterKeyTtl, app.masterKey);
141+
}
142+
143+
return app;
134144
})
135-
});
145+
);
146+
147+
response.apps = processedApps.filter((app) => app !== null);
136148
}
137149
// They provided correct auth
138150
return res.json(response);
@@ -147,6 +159,14 @@ module.exports = function(config, options) {
147159
//(ie. didn't supply usernames and passwords)
148160
if (requestIsLocal || options.dev) {
149161
//Allow no-auth access on localhost only, if they have configured the dashboard to not need auth
162+
await Promise.all(
163+
response.apps.map(async (app) => {
164+
if (typeof app.masterKey === 'function') {
165+
app.masterKey = await ConfigKeyCache.get(app.appId, 'masterKey', app.masterKeyTtl, app.masterKey);
166+
}
167+
})
168+
);
169+
150170
return res.json(response);
151171
}
152172
//We shouldn't get here. Fail closed.

Parse-Dashboard/configKeyCache.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class KeyCache {
2+
constructor() {
3+
this.cache = {};
4+
}
5+
6+
async get(appId, key, ttl, callback) {
7+
key = `${appId}:${key}`;
8+
const cached = this.cache[key];
9+
if (cached && cached.expiry > Date.now()) {
10+
return cached.value;
11+
}
12+
13+
const value = await Promise.resolve(callback());
14+
this.cache[key] = {
15+
value,
16+
expiry: Date.now() + ttl,
17+
};
18+
return value;
19+
}
20+
}
21+
22+
module.exports = new KeyCache();

Parse-Dashboard/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const startServer = require('./server');
1313
const program = require('commander');
1414
program.option('--appId [appId]', 'the app Id of the app you would like to manage.');
1515
program.option('--masterKey [masterKey]', 'the master key of the app you would like to manage.');
16+
program.option('--masterKeyTtl [masterKeyTtl]', 'the master key ttl of the app you would like to manage.');
1617
program.option('--serverURL [serverURL]', 'the server url of the app you would like to manage.');
1718
program.option('--graphQLServerURL [graphQLServerURL]', 'the GraphQL server url of the app you would like to manage.');
1819
program.option('--dev', 'Enable development mode. This will disable authentication and allow non HTTPS connections. DO NOT ENABLE IN PRODUCTION SERVERS');

README.md

+5
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ Parse Dashboard is continuously tested with the most recent releases of Node.js
131131
| Parameter | Type | Optional | Default | Example | Description |
132132
|----------------------------------------|---------------------|----------|---------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
133133
| `apps` | Array<Object> | no | - | `[{ ... }, { ... }]` | The apps that are configured for the dashboard. |
134+
| `apps.appId` | String | yes | - | `"myAppId"` | The Application ID for your Parse Server instance. |
135+
| `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. |
136+
| `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. |
137+
| `apps.serverURL` | String | yes | - | `"http://localhost:1337/parse"` | The URL where your Parse Server is running. |
138+
| `apps.appName` | String | no | - | `"MyApp"` | The display name of the app in the dashboard. |
134139
| `infoPanel` | Array<Object> | yes | - | `[{ ... }, { ... }]` | The [info panel](#info-panel) configuration. |
135140
| `infoPanel[*].title` | String | no | - | `User Details` | The panel title. |
136141
| `infoPanel[*].classes` | Array<String> | no | - | `["_User"]` | The classes for which the info panel should be displayed. |

0 commit comments

Comments
 (0)