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 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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
@@ -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'),
|
||||
@@ -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);
|
||||
@@ -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) {
|
||||
@@ -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.' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user