Accept context via header X-Parse-Cloud-Context (#7437)
* 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>
This commit is contained in:
@@ -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 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)
|
- 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)
|
- 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
|
## 4.5.0
|
||||||
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)
|
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)
|
||||||
|
|||||||
@@ -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 () => {
|
it('should expose context in before and afterSave', async () => {
|
||||||
let calledBefore = false;
|
let calledBefore = false;
|
||||||
let calledAfter = false;
|
let calledAfter = false;
|
||||||
@@ -2804,6 +2999,26 @@ describe('afterLogin hook', () => {
|
|||||||
done();
|
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 () => {
|
it('should have access to context when saving a new object', async () => {
|
||||||
Parse.Cloud.beforeSave('TestObject', req => {
|
Parse.Cloud.beforeSave('TestObject', req => {
|
||||||
expect(req.context.a).toEqual('a');
|
expect(req.context.a).toEqual('a');
|
||||||
|
|||||||
@@ -25,6 +25,17 @@ const getMountForRequest = function (req) {
|
|||||||
export function handleParseHeaders(req, res, next) {
|
export function handleParseHeaders(req, res, next) {
|
||||||
var mount = getMountForRequest(req);
|
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 = {
|
var info = {
|
||||||
appId: req.get('X-Parse-Application-Id'),
|
appId: req.get('X-Parse-Application-Id'),
|
||||||
sessionToken: req.get('X-Parse-Session-Token'),
|
sessionToken: req.get('X-Parse-Session-Token'),
|
||||||
@@ -35,7 +46,7 @@ export function handleParseHeaders(req, res, next) {
|
|||||||
dotNetKey: req.get('X-Parse-Windows-Key'),
|
dotNetKey: req.get('X-Parse-Windows-Key'),
|
||||||
restAPIKey: req.get('X-Parse-REST-API-Key'),
|
restAPIKey: req.get('X-Parse-REST-API-Key'),
|
||||||
clientVersion: req.get('X-Parse-Client-Version'),
|
clientVersion: req.get('X-Parse-Client-Version'),
|
||||||
context: {},
|
context: context,
|
||||||
};
|
};
|
||||||
|
|
||||||
var basicAuth = httpAuth(req);
|
var basicAuth = httpAuth(req);
|
||||||
@@ -105,8 +116,19 @@ export function handleParseHeaders(req, res, next) {
|
|||||||
info.masterKey = req.body._MasterKey;
|
info.masterKey = req.body._MasterKey;
|
||||||
delete req.body._MasterKey;
|
delete req.body._MasterKey;
|
||||||
}
|
}
|
||||||
if (req.body._context && req.body._context instanceof Object) {
|
if (req.body._context) {
|
||||||
info.context = 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;
|
delete req.body._context;
|
||||||
}
|
}
|
||||||
if (req.body._ContentType) {
|
if (req.body._ContentType) {
|
||||||
@@ -454,3 +476,8 @@ function invalidRequest(req, res) {
|
|||||||
res.status(403);
|
res.status(403);
|
||||||
res.end('{"error":"unauthorized"}');
|
res.end('{"error":"unauthorized"}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function malformedContext(req, res) {
|
||||||
|
res.status(400);
|
||||||
|
res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' });
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user