Skip to content

Commit 2c48751

Browse files
authored
feat: Allow to identify readOnlyMasterKey invocation of Cloud Function via request.isReadOnly (#10100)
1 parent f0e3f32 commit 2c48751

File tree

5 files changed

+122
-5
lines changed

5 files changed

+122
-5
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ The client keys used with Parse are no longer necessary with Parse Server. If yo
305305

306306
<sub>(1) `Parse.Object.createdAt`, `Parse.Object.updatedAt`.</sub>
307307

308+
> [!NOTE]
309+
> In Cloud Code, both `masterKey` and `readOnlyMasterKey` set `request.master` to `true`. To distinguish between them, check `request.isReadOnly`. For example, use `request.master && !request.isReadOnly` to ensure full master key access.
310+
308311
## Email Verification and Password Reset
309312

310313
Verifying user email addresses and enabling password reset via email requires an email adapter. There are many email adapters provided and maintained by the community. The following is an example configuration with an example email adapter. See the [Parse Server Options][server-options] for more details and a full list of available options.

spec/rest.spec.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,102 @@ describe('read-only masterKey', () => {
13901390
expect(res.data.error).toBe('Permission denied');
13911391
}
13921392
});
1393+
1394+
it('should expose isReadOnly in Cloud Function request when using readOnlyMasterKey', async () => {
1395+
let receivedMaster;
1396+
let receivedIsReadOnly;
1397+
Parse.Cloud.define('checkReadOnly', req => {
1398+
receivedMaster = req.master;
1399+
receivedIsReadOnly = req.isReadOnly;
1400+
return 'ok';
1401+
});
1402+
1403+
await request({
1404+
method: 'POST',
1405+
url: `${Parse.serverURL}/functions/checkReadOnly`,
1406+
headers: {
1407+
'X-Parse-Application-Id': Parse.applicationId,
1408+
'X-Parse-Master-Key': 'read-only-test',
1409+
'Content-Type': 'application/json',
1410+
},
1411+
body: {},
1412+
});
1413+
1414+
expect(receivedMaster).toBe(true);
1415+
expect(receivedIsReadOnly).toBe(true);
1416+
});
1417+
1418+
it('should not set isReadOnly in Cloud Function request when using masterKey', async () => {
1419+
let receivedMaster;
1420+
let receivedIsReadOnly;
1421+
Parse.Cloud.define('checkNotReadOnly', req => {
1422+
receivedMaster = req.master;
1423+
receivedIsReadOnly = req.isReadOnly;
1424+
return 'ok';
1425+
});
1426+
1427+
await request({
1428+
method: 'POST',
1429+
url: `${Parse.serverURL}/functions/checkNotReadOnly`,
1430+
headers: {
1431+
'X-Parse-Application-Id': Parse.applicationId,
1432+
'X-Parse-Master-Key': Parse.masterKey,
1433+
'Content-Type': 'application/json',
1434+
},
1435+
body: {},
1436+
});
1437+
1438+
expect(receivedMaster).toBe(true);
1439+
expect(receivedIsReadOnly).toBe(false);
1440+
});
1441+
1442+
it('should expose isReadOnly in beforeFind trigger when using readOnlyMasterKey', async () => {
1443+
let receivedMaster;
1444+
let receivedIsReadOnly;
1445+
Parse.Cloud.beforeFind('ReadOnlyTriggerTest', req => {
1446+
receivedMaster = req.master;
1447+
receivedIsReadOnly = req.isReadOnly;
1448+
});
1449+
1450+
const obj = new Parse.Object('ReadOnlyTriggerTest');
1451+
await obj.save(null, { useMasterKey: true });
1452+
1453+
await request({
1454+
method: 'GET',
1455+
url: `${Parse.serverURL}/classes/ReadOnlyTriggerTest`,
1456+
headers: {
1457+
'X-Parse-Application-Id': Parse.applicationId,
1458+
'X-Parse-Master-Key': 'read-only-test',
1459+
},
1460+
});
1461+
1462+
expect(receivedMaster).toBe(true);
1463+
expect(receivedIsReadOnly).toBe(true);
1464+
});
1465+
1466+
it('should not set isReadOnly in beforeFind trigger when using masterKey', async () => {
1467+
let receivedMaster;
1468+
let receivedIsReadOnly;
1469+
Parse.Cloud.beforeFind('ReadOnlyTriggerTestNeg', req => {
1470+
receivedMaster = req.master;
1471+
receivedIsReadOnly = req.isReadOnly;
1472+
});
1473+
1474+
const obj = new Parse.Object('ReadOnlyTriggerTestNeg');
1475+
await obj.save(null, { useMasterKey: true });
1476+
1477+
await request({
1478+
method: 'GET',
1479+
url: `${Parse.serverURL}/classes/ReadOnlyTriggerTestNeg`,
1480+
headers: {
1481+
'X-Parse-Application-Id': Parse.applicationId,
1482+
'X-Parse-Master-Key': Parse.masterKey,
1483+
},
1484+
});
1485+
1486+
expect(receivedMaster).toBe(true);
1487+
expect(receivedIsReadOnly).toBe(false);
1488+
});
13931489
});
13941490

13951491
describe('rest context', () => {

src/Routers/FunctionsRouter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export class FunctionsRouter extends PromiseRouter {
176176
params: params,
177177
config: req.config,
178178
master: req.auth && req.auth.isMaster,
179+
isReadOnly: !!(req.auth && req.auth.isReadOnly),
179180
user: req.auth && req.auth.user,
180181
installationId: req.info.installationId,
181182
log: req.config.loggerController,

src/cloud-code/Parse.Cloud.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,8 @@ module.exports = ParseCloud;
730730
/**
731731
* @interface Parse.Cloud.TriggerRequest
732732
* @property {String} installationId If set, the installationId triggering the request.
733-
* @property {Boolean} master If true, means the master key was used.
733+
* @property {Boolean} master If true, means the master key or the read-only master key was used.
734+
* @property {Boolean} isReadOnly If true, means the read-only master key was used. This is a subset of `master`, so `master` will also be true. Use `master && !isReadOnly` to check for full master key access.
734735
* @property {Boolean} isChallenge If true, means the current request is originally triggered by an auth challenge.
735736
* @property {Parse.User} user If set, the user that made the request.
736737
* @property {Parse.Object} object The object triggering the hook.
@@ -745,7 +746,8 @@ module.exports = ParseCloud;
745746
/**
746747
* @interface Parse.Cloud.FileTriggerRequest
747748
* @property {String} installationId If set, the installationId triggering the request.
748-
* @property {Boolean} master If true, means the master key was used.
749+
* @property {Boolean} master If true, means the master key or the read-only master key was used.
750+
* @property {Boolean} isReadOnly If true, means the read-only master key was used. This is a subset of `master`, so `master` will also be true. Use `master && !isReadOnly` to check for full master key access.
749751
* @property {Parse.User} user If set, the user that made the request.
750752
* @property {Parse.File} file The file that triggered the hook.
751753
* @property {Integer} fileSize The size of the file in bytes.
@@ -784,7 +786,8 @@ module.exports = ParseCloud;
784786
/**
785787
* @interface Parse.Cloud.BeforeFindRequest
786788
* @property {String} installationId If set, the installationId triggering the request.
787-
* @property {Boolean} master If true, means the master key was used.
789+
* @property {Boolean} master If true, means the master key or the read-only master key was used.
790+
* @property {Boolean} isReadOnly If true, means the read-only master key was used. This is a subset of `master`, so `master` will also be true. Use `master && !isReadOnly` to check for full master key access.
788791
* @property {Parse.User} user If set, the user that made the request.
789792
* @property {Parse.Query} query The query triggering the hook.
790793
* @property {String} ip The IP address of the client making the request.
@@ -798,7 +801,8 @@ module.exports = ParseCloud;
798801
/**
799802
* @interface Parse.Cloud.AfterFindRequest
800803
* @property {String} installationId If set, the installationId triggering the request.
801-
* @property {Boolean} master If true, means the master key was used.
804+
* @property {Boolean} master If true, means the master key or the read-only master key was used.
805+
* @property {Boolean} isReadOnly If true, means the read-only master key was used. This is a subset of `master`, so `master` will also be true. Use `master && !isReadOnly` to check for full master key access.
802806
* @property {Parse.User} user If set, the user that made the request.
803807
* @property {Parse.Query} query The query triggering the hook.
804808
* @property {Array<Parse.Object>} results The results the query yielded.
@@ -812,7 +816,8 @@ module.exports = ParseCloud;
812816
/**
813817
* @interface Parse.Cloud.FunctionRequest
814818
* @property {String} installationId If set, the installationId triggering the request.
815-
* @property {Boolean} master If true, means the master key was used.
819+
* @property {Boolean} master If true, means the master key or the read-only master key was used.
820+
* @property {Boolean} isReadOnly If true, means the read-only master key was used. This is a subset of `master`, so `master` will also be true. Use `master && !isReadOnly` to check for full master key access.
816821
* @property {Parse.User} user If set, the user that made the request.
817822
* @property {Object} params The params passed to the cloud function.
818823
* @property {String} ip The IP address of the client making the request.

src/triggers.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export function getRequestObject(
268268
triggerName: triggerType,
269269
object: parseObject,
270270
master: false,
271+
isReadOnly: false,
271272
log: config.loggerController,
272273
headers: config.headers,
273274
ip: config.ip,
@@ -301,6 +302,9 @@ export function getRequestObject(
301302
if (auth.isMaster) {
302303
request['master'] = true;
303304
}
305+
if (auth.isReadOnly) {
306+
request['isReadOnly'] = true;
307+
}
304308
if (auth.user) {
305309
request['user'] = auth.user;
306310
}
@@ -317,6 +321,7 @@ export function getRequestQueryObject(triggerType, auth, query, count, config, c
317321
triggerName: triggerType,
318322
query,
319323
master: false,
324+
isReadOnly: false,
320325
count,
321326
log: config.loggerController,
322327
isGet,
@@ -332,6 +337,9 @@ export function getRequestQueryObject(triggerType, auth, query, count, config, c
332337
if (auth.isMaster) {
333338
request['master'] = true;
334339
}
340+
if (auth.isReadOnly) {
341+
request['isReadOnly'] = true;
342+
}
335343
if (auth.user) {
336344
request['user'] = auth.user;
337345
}
@@ -1019,6 +1027,7 @@ export function getRequestFileObject(triggerType, auth, fileObject, config) {
10191027
...fileObject,
10201028
triggerName: triggerType,
10211029
master: false,
1030+
isReadOnly: false,
10221031
log: config.loggerController,
10231032
headers: config.headers,
10241033
ip: config.ip,
@@ -1031,6 +1040,9 @@ export function getRequestFileObject(triggerType, auth, fileObject, config) {
10311040
if (auth.isMaster) {
10321041
request['master'] = true;
10331042
}
1043+
if (auth.isReadOnly) {
1044+
request['isReadOnly'] = true;
1045+
}
10341046
if (auth.user) {
10351047
request['user'] = auth.user;
10361048
}

0 commit comments

Comments
 (0)