Skip to content

Commit

Permalink
prevent users from modifying and deleting objects
Browse files Browse the repository at this point in the history
  • Loading branch information
ntraut committed May 6, 2022
1 parent 4b8fb62 commit 638549a
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 116 deletions.
84 changes: 15 additions & 69 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ There is two way of authenticating against the API, depending on the request you
- `Session Token`: available for any user with a Connect account
- `OAuth Token`: available for developers who have implemented the Connect OAuth flow in their app (and therefore have an access token for their users)

| Endpoint | Session Token | OAuth Token |
| ------------------------------- | :-----------: | :---------: |
| GET /classes/ClassName |||
| GET /classes/ClassName/objectId |||
| POST /classes/ClassName |||
| PUT /classes/ClassName/objectId |||
| Endpoint | Session Token | OAuth Token |
| ----------------------------------- | :-----------: | :---------: |
| GET /classes/ClassName |||
| GET /classes/ClassName/:objectId |||
| POST /classes/ClassName |||
| PUT /classes/ClassName/:objectId |||
| DELETE /classes/ClassName/:objectId |||

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

Expand Down Expand Up @@ -200,56 +201,6 @@ Response :
}
```
### <a name="update-object">Update object</a>
> ⚠️ Update requests can only be performed with an [OAuth token](#oauth-authentication)
To update an object send a PUT request to the endpoint `/parse/classes/:OBJECTNAME/:OBJECTID` :
```bash
OBJECT_ID=DFwP7JXoa0
curl --request PUT \
--url $CONNECT_URL/parse/classes/GameScore/$OBJECT_ID \
--header 'content-type: application/json' \
--header 'x-parse-application-id: '$PARSE_APPLICATION \
--header 'Authorization: Bearer '$access_token \
--data '{
"score":1338,
"playerName":"sample",
"cheatMode":false,
}'
Response :
{
"score": 1338,
"playerName": "sample",
"cheatMode": false,
"createdAt": "2019-07-15T14:06:53.659Z",
"updatedAt": "2019-07-15T15:04:42.884Z",
"objectId": "DFwP7JXoa0",
"applicationId": "[YOUR_APPLICATION_ID]",
"userId": "[YOUR_USER_ID]"
}
```
> ⚠️ **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** ⚠️
### <a name="delete-object">Delete object</a>
To delete an object send a DELETE request to the endpoint `/parse/classes/:OBJECTNAME/:OBJECTID` :
```bash
curl --request DELETE \
--url $CONNECT_URL/parse/classes/GameScore/DFwP7JXoa0 \
--header 'x-parse-application-id: '$PARSE_APPLICATION \
--header 'Authorization: Bearer '$access_token
Response:
{}
```
> ⚠️ **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** ⚠️
### <a name="get-object">Get object</a>
> 💡 Get requests can be performed either with an [OAuth token](#oauth-authentication) or with a [Session token](#authentication)
Expand Down Expand Up @@ -380,6 +331,14 @@ Response:
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.
### <a name="update-object">Update object</a>
⚠️ To ensure integrity of the log, developers are not allowed to modify objects they sent.
### <a name="delete-object">Delete object</a>
⚠️ 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/
### <a name="app-details">Getting an app details from their ID</a>
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`:
Expand Down Expand Up @@ -431,19 +390,6 @@ curl --request POST \
"playerName": "Sean Plott"
}
},
{
"method": "PUT",
"path": "/parse/classes/GameScore/JhKvT9HrWJ",
"body": {
"score": 1337,
"playerName": "Sean Plott 2"
}
},
{
"method": "DELETE",
"path": "/parse/classes/GameScore/RAdL53JiZV",
"body": {}
},
{
"method": "GET",
"path": "/parse/classes/GameScore/JhKvT9HrWJ",
Expand Down
81 changes: 36 additions & 45 deletions spec/api.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ describe('Parse server', () => {
})

let createdGameScoreObjectId;
let gameScoreObject;

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

createdGameScoreObjectId = data.objectId;
gameScoreObject = data;
});

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

let gameScoreObject;

it('PUT GameScore event', async () => {
const { data } = await axios({
method: 'put',
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
headers: {
'Content-Type': 'application/json',
'x-parse-application-id': PARSE_APP_ID,
Authorization: 'Bearer ' + accessToken.access_token,
},
data: { score: 1338, cheatMode: true },
});

expect(data.createdAt).toBeDefined();
expect(data.updatedAt).toBeDefined();
expect(data.objectId).toBeDefined();

expect(data).toEqual({
objectId: data.objectId,
createdAt: data.createdAt,
updatedAt: data.updatedAt,
score: 1338,
playerName: 'test9',
cheatMode: true,
applicationId: application.objectId,
userId: endUserUserId,
});

gameScoreObject = data;
});

it('GET GameScore event using OAuth', async () => {
const { data } = await axios({
method: 'get',
Expand Down Expand Up @@ -468,6 +438,24 @@ describe('Parse server', () => {
expect(data.results[0]).toEqual(gameScoreObject);
});

it('refuse PUT GameScore event even for the creator of the object', async () => {
expect.assertions(1);
try {
await axios({
method: 'put',
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
headers: {
'Content-Type': 'application/json',
'x-parse-application-id': PARSE_APP_ID,
Authorization: 'Bearer ' + accessToken.access_token,
},
data: { score: 1338, cheatMode: true },
});
} catch (err) {
expect(err).toBeDefined();
}
});

it('refuse PUT GameScore event when owner on a different application', async () => {
const { accessToken: accessTokenApp2 } = await getAccessToken(
credentials.endUser.email,
Expand Down Expand Up @@ -593,7 +581,7 @@ describe('Parse server', () => {
}
});

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

it('DELETE GameScore event', async () => {
const { data } = await axios({
method: 'delete',
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
headers: {
'Content-Type': 'application/json',
'x-parse-application-id': PARSE_APP_ID,
Authorization: 'Bearer ' + accessToken.access_token,
},
});

expect(data).toEqual({});
it('refuse DELETE GameScore event even for the creator on the same application', async () => {
expect.assertions(1);
try {
await axios({
method: 'delete',
url: `${API_URL}/parse/classes/GameScore/${createdGameScoreObjectId}`,
headers: {
'Content-Type': 'application/json',
'x-parse-application-id': PARSE_APP_ID,
Authorization: 'Bearer ' + accessToken.access_token,
},
});
} catch (err) {
expect(err).toBeDefined();
}
});
});
5 changes: 5 additions & 0 deletions src/parse/cloud/setBeforeDelete.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ module.exports = async (Parse) => {
const schemaClasses = await getClasses();
for (const schemaClass of schemaClasses) {
Parse.Cloud.beforeDelete(schemaClass.className, async (req) => {
// was used to check that the user requesting the deletion was the creator of the object
// no longer used because deletion is now prevented by CLP
if (req.master) {
return;
}
if (!req.user) {
// user is not authenticated, Forbidden.
throw new Error('User should be authenticated.');
Expand Down
4 changes: 4 additions & 0 deletions src/parse/cloud/setBeforeSave.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ module.exports = async (Parse) => {
for (const schemaClass of schemaClasses) {
// eslint-disable-next-line max-statements
Parse.Cloud.beforeSave(schemaClass.className, async (req) => {
if (req.master) {
return;
}
if (!req.user) {
// user is not authenticated, Forbidden.
throw new Error('User should be authenticated.');
Expand Down Expand Up @@ -78,6 +81,7 @@ module.exports = async (Parse) => {
req.object.set('userId', endUser.id);
req.object.set('applicationId', application.id);

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

roleACL.setReadAccess(req.user, true);
Expand Down
4 changes: 2 additions & 2 deletions src/parse/schema/sanitizeClass.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ module.exports = (schemaClass) => {
find: { 'role:Developer': true, 'role:Administrator': true },
get: { 'role:Developer': true, 'role:Administrator': true },
create: { 'role:Developer': true, 'role:Administrator': true },
update: { 'role:Developer': true, 'role:Administrator': true },
delete: { 'role:Developer': true, 'role:Administrator': true },
update: { 'role:Administrator': true },
delete: { 'role:Administrator': true },
};

return newSchemaClass;
Expand Down

1 comment on commit 638549a

@ntraut
Copy link
Member Author

@ntraut ntraut commented on 638549a May 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix issue #50

Please sign in to comment.