Skip to content

Commit 638549a

Browse files
committed
prevent users from modifying and deleting objects
1 parent 4b8fb62 commit 638549a

File tree

5 files changed

+62
-116
lines changed

5 files changed

+62
-116
lines changed

docs/usage.md

Lines changed: 15 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ There is two way of authenticating against the API, depending on the request you
5454
- `Session Token`: available for any user with a Connect account
5555
- `OAuth Token`: available for developers who have implemented the Connect OAuth flow in their app (and therefore have an access token for their users)
5656

57-
| Endpoint | Session Token | OAuth Token |
58-
| ------------------------------- | :-----------: | :---------: |
59-
| GET /classes/ClassName |||
60-
| GET /classes/ClassName/objectId |||
61-
| POST /classes/ClassName |||
62-
| PUT /classes/ClassName/objectId |||
57+
| Endpoint | Session Token | OAuth Token |
58+
| ----------------------------------- | :-----------: | :---------: |
59+
| GET /classes/ClassName |||
60+
| GET /classes/ClassName/:objectId |||
61+
| POST /classes/ClassName |||
62+
| PUT /classes/ClassName/:objectId |||
63+
| DELETE /classes/ClassName/:objectId |||
6364

6465
### <a name="authentication">Session token Authentication</a>
6566

@@ -200,56 +201,6 @@ Response :
200201
}
201202
```
202203
203-
### <a name="update-object">Update object</a>
204-
205-
> ⚠️ Update requests can only be performed with an [OAuth token](#oauth-authentication)
206-
207-
To update an object send a PUT request to the endpoint `/parse/classes/:OBJECTNAME/:OBJECTID` :
208-
209-
```bash
210-
OBJECT_ID=DFwP7JXoa0
211-
curl --request PUT \
212-
--url $CONNECT_URL/parse/classes/GameScore/$OBJECT_ID \
213-
--header 'content-type: application/json' \
214-
--header 'x-parse-application-id: '$PARSE_APPLICATION \
215-
--header 'Authorization: Bearer '$access_token \
216-
--data '{
217-
"score":1338,
218-
"playerName":"sample",
219-
"cheatMode":false,
220-
}'
221-
222-
Response :
223-
{
224-
"score": 1338,
225-
"playerName": "sample",
226-
"cheatMode": false,
227-
"createdAt": "2019-07-15T14:06:53.659Z",
228-
"updatedAt": "2019-07-15T15:04:42.884Z",
229-
"objectId": "DFwP7JXoa0",
230-
"applicationId": "[YOUR_APPLICATION_ID]",
231-
"userId": "[YOUR_USER_ID]"
232-
}
233-
```
234-
235-
> ⚠️ **Only the owner of the data can update an object. If you did not create this object with the same user, you will have an error message** ⚠️
236-
237-
### <a name="delete-object">Delete object</a>
238-
239-
To delete an object send a DELETE request to the endpoint `/parse/classes/:OBJECTNAME/:OBJECTID` :
240-
241-
```bash
242-
curl --request DELETE \
243-
--url $CONNECT_URL/parse/classes/GameScore/DFwP7JXoa0 \
244-
--header 'x-parse-application-id: '$PARSE_APPLICATION \
245-
--header 'Authorization: Bearer '$access_token
246-
247-
Response:
248-
{}
249-
```
250-
251-
> ⚠️ **Like for update, only the owner of the data can delete an object. If you did not create this object you will have an error message** ⚠️
252-
253204
### <a name="get-object">Get object</a>
254205
255206
> 💡 Get requests can be performed either with an [OAuth token](#oauth-authentication) or with a [Session token](#authentication)
@@ -380,6 +331,14 @@ Response:
380331
381332
Since this requests a count as well as limiting to zero results, there will be a count but no results in the response. With a nonzero limit, that request would return results as well as the count.
382333
334+
### <a name="update-object">Update object</a>
335+
336+
⚠️ To ensure integrity of the log, developers are not allowed to modify objects they sent.
337+
338+
### <a name="delete-object">Delete object</a>
339+
340+
⚠️ Like for modification, developers are not allowed to modify objects they sent. If for some reasons you need to remove some objects, you should contact the administrators/
341+
383342
### <a name="app-details">Getting an app details from their ID</a>
384343
385344
When you consult data, each object will be returned with an attribute `applicationId`. If needed, it is possible to fetch the name and description of the app using the class `OAuthApplication`:
@@ -431,19 +390,6 @@ curl --request POST \
431390
"playerName": "Sean Plott"
432391
}
433392
},
434-
{
435-
"method": "PUT",
436-
"path": "/parse/classes/GameScore/JhKvT9HrWJ",
437-
"body": {
438-
"score": 1337,
439-
"playerName": "Sean Plott 2"
440-
}
441-
},
442-
{
443-
"method": "DELETE",
444-
"path": "/parse/classes/GameScore/RAdL53JiZV",
445-
"body": {}
446-
},
447393
{
448394
"method": "GET",
449395
"path": "/parse/classes/GameScore/JhKvT9HrWJ",

spec/api.spec.js

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ describe('Parse server', () => {
299299
})
300300

301301
let createdGameScoreObjectId;
302+
let gameScoreObject;
302303

303304
it('POST GameScore event', async () => {
304305
const { data } = await axios({
@@ -328,6 +329,7 @@ describe('Parse server', () => {
328329
});
329330

330331
createdGameScoreObjectId = data.objectId;
332+
gameScoreObject = data;
331333
});
332334

333335
it('POST batch GameScore event', async () => {
@@ -360,38 +362,6 @@ describe('Parse server', () => {
360362
expect(data[1].success.score).toBe(3946);
361363
});
362364

363-
let gameScoreObject;
364-
365-
it('PUT GameScore event', async () => {
366-
const { data } = await axios({
367-
method: 'put',
368-
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
369-
headers: {
370-
'Content-Type': 'application/json',
371-
'x-parse-application-id': PARSE_APP_ID,
372-
Authorization: 'Bearer ' + accessToken.access_token,
373-
},
374-
data: { score: 1338, cheatMode: true },
375-
});
376-
377-
expect(data.createdAt).toBeDefined();
378-
expect(data.updatedAt).toBeDefined();
379-
expect(data.objectId).toBeDefined();
380-
381-
expect(data).toEqual({
382-
objectId: data.objectId,
383-
createdAt: data.createdAt,
384-
updatedAt: data.updatedAt,
385-
score: 1338,
386-
playerName: 'test9',
387-
cheatMode: true,
388-
applicationId: application.objectId,
389-
userId: endUserUserId,
390-
});
391-
392-
gameScoreObject = data;
393-
});
394-
395365
it('GET GameScore event using OAuth', async () => {
396366
const { data } = await axios({
397367
method: 'get',
@@ -468,6 +438,24 @@ describe('Parse server', () => {
468438
expect(data.results[0]).toEqual(gameScoreObject);
469439
});
470440

441+
it('refuse PUT GameScore event even for the creator of the object', async () => {
442+
expect.assertions(1);
443+
try {
444+
await axios({
445+
method: 'put',
446+
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
447+
headers: {
448+
'Content-Type': 'application/json',
449+
'x-parse-application-id': PARSE_APP_ID,
450+
Authorization: 'Bearer ' + accessToken.access_token,
451+
},
452+
data: { score: 1338, cheatMode: true },
453+
});
454+
} catch (err) {
455+
expect(err).toBeDefined();
456+
}
457+
});
458+
471459
it('refuse PUT GameScore event when owner on a different application', async () => {
472460
const { accessToken: accessTokenApp2 } = await getAccessToken(
473461
credentials.endUser.email,
@@ -593,7 +581,7 @@ describe('Parse server', () => {
593581
}
594582
});
595583

596-
it('refuse DELETE GameScore event when owner on a different application', async () => {
584+
it('refuse DELETE GameScore event when creator on a different application', async () => {
597585
const { accessToken: accessTokenApp2 } = await getAccessToken(
598586
credentials.endUser.email,
599587
credentials.endUser.password,
@@ -615,17 +603,20 @@ describe('Parse server', () => {
615603
}
616604
});
617605

618-
it('DELETE GameScore event', async () => {
619-
const { data } = await axios({
620-
method: 'delete',
621-
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
622-
headers: {
623-
'Content-Type': 'application/json',
624-
'x-parse-application-id': PARSE_APP_ID,
625-
Authorization: 'Bearer ' + accessToken.access_token,
626-
},
627-
});
628-
629-
expect(data).toEqual({});
606+
it('refuse DELETE GameScore event even for the creator on the same application', async () => {
607+
expect.assertions(1);
608+
try {
609+
await axios({
610+
method: 'delete',
611+
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
612+
headers: {
613+
'Content-Type': 'application/json',
614+
'x-parse-application-id': PARSE_APP_ID,
615+
Authorization: 'Bearer ' + accessToken.access_token,
616+
},
617+
});
618+
} catch (err) {
619+
expect(err).toBeDefined();
620+
}
630621
});
631622
});

src/parse/cloud/setBeforeDelete.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ module.exports = async (Parse) => {
77
const schemaClasses = await getClasses();
88
for (const schemaClass of schemaClasses) {
99
Parse.Cloud.beforeDelete(schemaClass.className, async (req) => {
10+
// was used to check that the user requesting the deletion was the creator of the object
11+
// no longer used because deletion is now prevented by CLP
12+
if (req.master) {
13+
return;
14+
}
1015
if (!req.user) {
1116
// user is not authenticated, Forbidden.
1217
throw new Error('User should be authenticated.');

src/parse/cloud/setBeforeSave.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ module.exports = async (Parse) => {
1212
for (const schemaClass of schemaClasses) {
1313
// eslint-disable-next-line max-statements
1414
Parse.Cloud.beforeSave(schemaClass.className, async (req) => {
15+
if (req.master) {
16+
return;
17+
}
1518
if (!req.user) {
1619
// user is not authenticated, Forbidden.
1720
throw new Error('User should be authenticated.');
@@ -78,6 +81,7 @@ module.exports = async (Parse) => {
7881
req.object.set('userId', endUser.id);
7982
req.object.set('applicationId', application.id);
8083

84+
// Those ACL should not be useful as CLP are more restrictive
8185
const roleACL = new Parse.ACL();
8286

8387
roleACL.setReadAccess(req.user, true);

src/parse/schema/sanitizeClass.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ module.exports = (schemaClass) => {
1515
find: { 'role:Developer': true, 'role:Administrator': true },
1616
get: { 'role:Developer': true, 'role:Administrator': true },
1717
create: { 'role:Developer': true, 'role:Administrator': true },
18-
update: { 'role:Developer': true, 'role:Administrator': true },
19-
delete: { 'role:Developer': true, 'role:Administrator': true },
18+
update: { 'role:Administrator': true },
19+
delete: { 'role:Administrator': true },
2020
};
2121

2222
return newSchemaClass;

0 commit comments

Comments
 (0)