Skip to content

Commit

Permalink
Accept context via header X-Parse-Cloud-Context (#7437)
Browse files Browse the repository at this point in the history
* failing testcase

* add header

* switch to X-Parse-Cloud-Context header

* add back blank line that lint removed

* test replacing context header with body context. Add support for setting body with json string

* add back blank line

* cover error when _context body is wrong

* Update middlewares.js

* revert accidental status change

* make sure context always decodes to an object else throw error

* improve context object check

Co-authored-by: Antonio Davi Macedo Coelho de Castro <adavimacedo@gmail.com>
  • Loading branch information
cbaker6 and davimacedo authored Jul 26, 2021
1 parent c3b71ba commit c8e822b
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 3 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ ___
- Fix select and excludeKey queries to properly accept JSON string arrays. Also allow nested fields in exclude (Corey Baker) [#7242](https://github.com/parse-community/parse-server/pull/7242)
- Fix LiveQuery server crash when using $all query operator on a missing object key (Jason Posthuma) [#7421](https://github.com/parse-community/parse-server/pull/7421)
- Added runtime deprecation warnings (Manuel Trezza) [#7451](https://github.com/parse-community/parse-server/pull/7451)
- Add ability to pass context of an object via a header, X-Parse-Cloud-Context, for Cloud Code triggers. The header addition allows client SDK's to add context without injecting _context in the body of JSON objects (Corey Baker) [#7437](https://github.com/parse-community/parse-server/pull/7437)

___
## 4.5.0
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)
Expand Down
215 changes: 215 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2519,6 +2519,201 @@ describe('afterFind hooks', () => {
});
});

it('should throw error if context header is malformed', async () => {
let calledBefore = false;
let calledAfter = false;
Parse.Cloud.beforeSave('TestObject', () => {
calledBefore = true;
});
Parse.Cloud.afterSave('TestObject', () => {
calledAfter = true;
});
const req = request({
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Cloud-Context': 'key',
},
body: {
foo: 'bar',
},
});
try {
await req;
fail('Should have thrown error');
} catch (e) {
expect(e).toBeDefined();
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
}
expect(calledBefore).toBe(false);
expect(calledAfter).toBe(false);
});

it('should throw error if context header is string "1"', async () => {
let calledBefore = false;
let calledAfter = false;
Parse.Cloud.beforeSave('TestObject', () => {
calledBefore = true;
});
Parse.Cloud.afterSave('TestObject', () => {
calledAfter = true;
});
const req = request({
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Cloud-Context': '1',
},
body: {
foo: 'bar',
},
});
try {
await req;
fail('Should have thrown error');
} catch (e) {
expect(e).toBeDefined();
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
}
expect(calledBefore).toBe(false);
expect(calledAfter).toBe(false);
});

it('should expose context in beforeSave/afterSave via header', async () => {
let calledBefore = false;
let calledAfter = false;
Parse.Cloud.beforeSave('TestObject', req => {
expect(req.object.get('foo')).toEqual('bar');
expect(req.context.otherKey).toBe(1);
expect(req.context.key).toBe('value');
calledBefore = true;
});
Parse.Cloud.afterSave('TestObject', req => {
expect(req.object.get('foo')).toEqual('bar');
expect(req.context.otherKey).toBe(1);
expect(req.context.key).toBe('value');
calledAfter = true;
});
const req = request({
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
},
body: {
foo: 'bar',
},
});
await req;
expect(calledBefore).toBe(true);
expect(calledAfter).toBe(true);
});

it('should override header context with body context in beforeSave/afterSave', async () => {
let calledBefore = false;
let calledAfter = false;
Parse.Cloud.beforeSave('TestObject', req => {
expect(req.object.get('foo')).toEqual('bar');
expect(req.context.otherKey).toBe(10);
expect(req.context.key).toBe('hello');
calledBefore = true;
});
Parse.Cloud.afterSave('TestObject', req => {
expect(req.object.get('foo')).toEqual('bar');
expect(req.context.otherKey).toBe(10);
expect(req.context.key).toBe('hello');
calledAfter = true;
});
const req = request({
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
},
body: {
foo: 'bar',
_ApplicationId: 'test',
_context: '{"key":"hello","otherKey":10}',
},
});
await req;
expect(calledBefore).toBe(true);
expect(calledAfter).toBe(true);
});

it('should throw error if context body is malformed', async () => {
let calledBefore = false;
let calledAfter = false;
Parse.Cloud.beforeSave('TestObject', () => {
calledBefore = true;
});
Parse.Cloud.afterSave('TestObject', () => {
calledAfter = true;
});
const req = request({
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
},
body: {
foo: 'bar',
_ApplicationId: 'test',
_context: 'key',
},
});
try {
await req;
fail('Should have thrown error');
} catch (e) {
expect(e).toBeDefined();
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
}
expect(calledBefore).toBe(false);
expect(calledAfter).toBe(false);
});

it('should throw error if context body is string "true"', async () => {
let calledBefore = false;
let calledAfter = false;
Parse.Cloud.beforeSave('TestObject', () => {
calledBefore = true;
});
Parse.Cloud.afterSave('TestObject', () => {
calledAfter = true;
});
const req = request({
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
headers: {
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
},
body: {
foo: 'bar',
_ApplicationId: 'test',
_context: 'true',
},
});
try {
await req;
fail('Should have thrown error');
} catch (e) {
expect(e).toBeDefined();
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
}
expect(calledBefore).toBe(false);
expect(calledAfter).toBe(false);
});

it('should expose context in before and afterSave', async () => {
let calledBefore = false;
let calledAfter = false;
Expand Down Expand Up @@ -2804,6 +2999,26 @@ describe('afterLogin hook', () => {
done();
});

it('context options should override _context object property when saving a new object', async () => {
Parse.Cloud.beforeSave('TestObject', req => {
expect(req.context.a).toEqual('a');
expect(req.context.hello).not.toBeDefined();
expect(req._context).not.toBeDefined();
expect(req.object._context).not.toBeDefined();
expect(req.object.context).not.toBeDefined();
});
Parse.Cloud.afterSave('TestObject', req => {
expect(req.context.a).toEqual('a');
expect(req.context.hello).not.toBeDefined();
expect(req._context).not.toBeDefined();
expect(req.object._context).not.toBeDefined();
expect(req.object.context).not.toBeDefined();
});
const obj = new TestObject();
obj.set('_context', { hello: 'world' });
await obj.save(null, { context: { a: 'a' } });
});

it('should have access to context when saving a new object', async () => {
Parse.Cloud.beforeSave('TestObject', req => {
expect(req.context.a).toEqual('a');
Expand Down
33 changes: 30 additions & 3 deletions src/middlewares.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ const getMountForRequest = function (req) {
export function handleParseHeaders(req, res, next) {
var mount = getMountForRequest(req);

let context = {};
if (req.get('X-Parse-Cloud-Context') != null) {
try {
context = JSON.parse(req.get('X-Parse-Cloud-Context'));
if (Object.prototype.toString.call(context) !== '[object Object]') {
throw 'Context is not an object';
}
} catch (e) {
return malformedContext(req, res);
}
}
var info = {
appId: req.get('X-Parse-Application-Id'),
sessionToken: req.get('X-Parse-Session-Token'),
Expand All @@ -35,7 +46,7 @@ export function handleParseHeaders(req, res, next) {
dotNetKey: req.get('X-Parse-Windows-Key'),
restAPIKey: req.get('X-Parse-REST-API-Key'),
clientVersion: req.get('X-Parse-Client-Version'),
context: {},
context: context,
};

var basicAuth = httpAuth(req);
Expand Down Expand Up @@ -105,8 +116,19 @@ export function handleParseHeaders(req, res, next) {
info.masterKey = req.body._MasterKey;
delete req.body._MasterKey;
}
if (req.body._context && req.body._context instanceof Object) {
info.context = req.body._context;
if (req.body._context) {
if (req.body._context instanceof Object) {
info.context = req.body._context;
} else {
try {
info.context = JSON.parse(req.body._context);
if (Object.prototype.toString.call(info.context) !== '[object Object]') {
throw 'Context is not an object';
}
} catch (e) {
return malformedContext(req, res);
}
}
delete req.body._context;
}
if (req.body._ContentType) {
Expand Down Expand Up @@ -454,3 +476,8 @@ function invalidRequest(req, res) {
res.status(403);
res.end('{"error":"unauthorized"}');
}

function malformedContext(req, res) {
res.status(400);
res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' });
}

0 comments on commit c8e822b

Please sign in to comment.