Skip to content

Commit 9733968

Browse files
committed
Adds support for readOnly masterKey (#787)
* Adds support for readOnly masterKey * nits * Adds tests for readOnly * Updates documentation * nit * nits
1 parent a9612af commit 9733968

File tree

4 files changed

+131
-8
lines changed

4 files changed

+131
-8
lines changed

Parse-Dashboard/Authentication.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ function initialize(app, options) {
7979
* @returns {Object} Object with `isAuthenticated` and `appsUserHasAccessTo` properties
8080
*/
8181
function authenticate(userToTest, usernameOnly) {
82-
var appsUserHasAccessTo = null;
83-
var matchingUsername = null;
82+
let appsUserHasAccessTo = null;
83+
let matchingUsername = null;
84+
let isReadOnly = false;
8485

8586
//they provided auth
8687
let isAuthenticated = userToTest &&
@@ -96,6 +97,7 @@ function authenticate(userToTest, usernameOnly) {
9697
matchingUsername = user.user;
9798
// User restricted apps
9899
appsUserHasAccessTo = user.apps || null;
100+
isReadOnly = !!user.readOnly; // make it true/false
99101
}
100102

101103
return isAuthenticated;
@@ -104,7 +106,8 @@ function authenticate(userToTest, usernameOnly) {
104106
return {
105107
isAuthenticated,
106108
matchingUsername,
107-
appsUserHasAccessTo
109+
appsUserHasAccessTo,
110+
isReadOnly,
108111
};
109112
}
110113

Parse-Dashboard/app.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,11 @@ module.exports = function(config, options) {
7676

7777
// Serve the configuration.
7878
app.get('/parse-dashboard-config.json', function(req, res) {
79+
// Copy the apps
80+
const apps = config.apps.map((app) => Object.assign({}, app));
7981
let response = {
80-
apps: config.apps,
81-
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
82+
apps,
83+
newFeaturesInLatestVersion,
8284
};
8385

8486
//Based on advice from Doug Wilson here:
@@ -101,14 +103,29 @@ module.exports = function(config, options) {
101103

102104
const successfulAuth = authentication && authentication.isAuthenticated;
103105
const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
106+
const isReadOnly = authentication && authentication.isReadOnly;
107+
// User is full read-only, replace the masterKey by the read-only one
108+
if (isReadOnly) {
109+
response.apps = response.apps.map((app) => {
110+
app.masterKey = app.readOnlyMasterKey;
111+
if (!app.masterKey) {
112+
throw new Error('You need to provide a readOnlyMasterKey to use read-only features.');
113+
}
114+
return app;
115+
});
116+
}
104117

105118
if (successfulAuth) {
106119
if (appsUserHasAccess) {
107120
// Restric access to apps defined in user dictionary
108121
// If they didn't supply any app id, user will access all apps
109122
response.apps = response.apps.filter(function (app) {
110123
return appsUserHasAccess.find(appUserHasAccess => {
111-
return app.appId == appUserHasAccess.appId
124+
const isSame = app.appId === appUserHasAccess.appId;
125+
if (isSame && appUserHasAccess.readOnly) {
126+
app.masterKey = app.readOnlyMasterKey;
127+
}
128+
return isSame;
112129
})
113130
});
114131
}

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,80 @@ When `user1` logs in, he/she will be able to manage `myAppId1` and `myAppId2` fr
282282

283283
When *`user2`* logs in, he/she will only be able to manage *`myAppId1`* from the dashboard.
284284

285+
## Use Read-Only masterKey
286+
287+
Starting parse-server 2.6.5, it is possible to provide a `readOnlyMasterKey` to parse-server to prevent mutations on objects from a client.
288+
If you want to protect your dashboard with this feature, just use the `readOnlyMasterKey` instead of the `masterKey`. All write calls will fail.
289+
290+
### Making an app read-only for all users
291+
292+
Start your `parse-server` with
293+
294+
```json
295+
{
296+
"masterKey": "YOUR_MASTER_KEY_HERE",
297+
"readOnlyMasterKey": "YOUR_READ_ONLY_MASTER_KEY",
298+
}
299+
```
300+
301+
Then in your dashboard configuration:
302+
303+
```
304+
var trustProxy = true;
305+
var dashboard = new ParseDashboard({
306+
"apps": [
307+
{
308+
"serverURL": "http://localhost:1337/parse",
309+
"appId": "myAppId",
310+
"masterKey": "YOUR_READ_ONLY_MASTER_KEY",
311+
"appName": "MyApp"
312+
}
313+
],
314+
"trustProxy": 1
315+
});
316+
```
317+
318+
### Makings users read-only
319+
320+
You can mark a user as a read-only user:
321+
322+
```json
323+
{
324+
"apps": [{"...": "..."}],
325+
"users": [
326+
{
327+
"user":"user1",
328+
"pass":"pass1",
329+
"readOnly": true,
330+
"apps": [{"appId": "myAppId1"}, {"appId": "myAppId2"}]
331+
},
332+
{
333+
"user":"user2",
334+
"pass":"pass2",
335+
"apps": [{"appId": "myAppId1"}]
336+
} ]
337+
}
338+
```
339+
340+
This way `user1` will have a readOnly access to `myAppId1` and `myAppId2`
341+
342+
### Making user's apps readOnly
343+
344+
You can give read only access to a user on a per-app basis:
345+
346+
```json
347+
{
348+
"apps": [{"...": "..."}],
349+
"users": [
350+
{
351+
"user":"user1",
352+
"pass":"pass1",
353+
"apps": [{"appId": "myAppId1", "readOnly": true}, {"appId": "myAppId2"}]
354+
} ]
355+
}
356+
```
357+
358+
With this configuration, user1 will have read only access to `myAppId1` and read/write access to `myAppId2`.
285359

286360
## Run with Docker
287361

src/lib/tests/Authentication.test.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ jest.dontMock('bcryptjs');
1010

1111
const Authentication = require('../../../Parse-Dashboard/Authentication');
1212
const apps = [{appId: 'test123'}, {appId: 'test789'}];
13+
const readOnlyApps = apps.map((app) => {
14+
app.readOnly = true;
15+
return app;
16+
});
17+
1318
const unencryptedUsers = [
1419
{
1520
user: 'parse.dashboard',
@@ -19,6 +24,16 @@ const unencryptedUsers = [
1924
user: 'parse.apps',
2025
pass: 'xyz789',
2126
apps: apps
27+
},
28+
{
29+
user: 'parse.readonly',
30+
pass: 'abc123',
31+
readOnly: true,
32+
},
33+
{
34+
user: 'parse.readonly.apps',
35+
pass: 'abc123',
36+
apps: readOnlyApps,
2237
}
2338
];
2439
const encryptedUsers = [
@@ -33,11 +48,13 @@ const encryptedUsers = [
3348
}
3449
]
3550

36-
function createAuthenticationResult(isAuthenticated, matchingUsername, appsUserHasAccessTo) {
51+
function createAuthenticationResult(isAuthenticated, matchingUsername, appsUserHasAccessTo, isReadOnly) {
52+
isReadOnly = !!isReadOnly;
3753
return {
3854
isAuthenticated,
3955
matchingUsername,
40-
appsUserHasAccessTo
56+
appsUserHasAccessTo,
57+
isReadOnly
4158
}
4259
}
4360

@@ -107,4 +124,16 @@ describe('Authentication', () => {
107124
expect(authentication.authenticate({name: 'parse.dashboard'}, true))
108125
.toEqual(createAuthenticationResult(true, 'parse.dashboard', null));
109126
});
127+
128+
it('makes readOnly auth when specified', () => {
129+
let authentication = new Authentication(unencryptedUsers, false);
130+
expect(authentication.authenticate({name: 'parse.readonly', pass: 'abc123'}))
131+
.toEqual(createAuthenticationResult(true, 'parse.readonly', null, true));
132+
});
133+
134+
it('makes readOnly auth when specified in apps', () => {
135+
let authentication = new Authentication(unencryptedUsers, false);
136+
expect(authentication.authenticate({name: 'parse.readonly.apps', pass: 'abc123'}))
137+
.toEqual(createAuthenticationResult(true, 'parse.readonly.apps', readOnlyApps, false));
138+
});
110139
});

0 commit comments

Comments
 (0)