Skip to content

Commit 67d3c97

Browse files
authored
Adds support for readOnly masterKey (#787)
* Adds support for readOnly masterKey * nits * Adds tests for readOnly * Updates documentation * nit * nits
1 parent d8788f0 commit 67d3c97

File tree

4 files changed

+128
-7
lines changed

4 files changed

+128
-7
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: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ module.exports = function(config, options) {
7777
// Serve the configuration.
7878
app.get('/parse-dashboard-config.json', function(req, res) {
7979
let response = {
80-
apps: config.apps,
80+
apps: [...config.apps], // make a copy
8181
newFeaturesInLatestVersion: newFeaturesInLatestVersion,
8282
};
8383

@@ -101,14 +101,29 @@ module.exports = function(config, options) {
101101

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

105116
if (successfulAuth) {
106117
if (appsUserHasAccess) {
107118
// Restric access to apps defined in user dictionary
108119
// If they didn't supply any app id, user will access all apps
109120
response.apps = response.apps.filter(function (app) {
110121
return appsUserHasAccess.find(appUserHasAccess => {
111-
return app.appId == appUserHasAccess.appId
122+
const isSame = app.appId === appUserHasAccess.appId;
123+
if (isSame && appUserHasAccess.readOnly) {
124+
app.masterKey = app.readOnlyMasterKey;
125+
}
126+
return isSame;
112127
})
113128
});
114129
}

README.md

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

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

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

288362
## Run with Docker
289363

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)