Skip to content

Commit c8e822b

Browse files
cbaker6davimacedo
andauthored
Accept context via header X-Parse-Cloud-Context (parse-community#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 <[email protected]>
1 parent c3b71ba commit c8e822b

File tree

3 files changed

+247
-3
lines changed

3 files changed

+247
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ ___
137137
- 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)
138138
- 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)
139139
- Added runtime deprecation warnings (Manuel Trezza) [#7451](https://github.com/parse-community/parse-server/pull/7451)
140+
- 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)
141+
140142
___
141143
## 4.5.0
142144
[Full Changelog](https://github.com/parse-community/parse-server/compare/4.4.0...4.5.0)

spec/CloudCode.spec.js

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2519,6 +2519,201 @@ describe('afterFind hooks', () => {
25192519
});
25202520
});
25212521

2522+
it('should throw error if context header is malformed', async () => {
2523+
let calledBefore = false;
2524+
let calledAfter = false;
2525+
Parse.Cloud.beforeSave('TestObject', () => {
2526+
calledBefore = true;
2527+
});
2528+
Parse.Cloud.afterSave('TestObject', () => {
2529+
calledAfter = true;
2530+
});
2531+
const req = request({
2532+
method: 'POST',
2533+
url: 'http://localhost:8378/1/classes/TestObject',
2534+
headers: {
2535+
'X-Parse-Application-Id': 'test',
2536+
'X-Parse-REST-API-Key': 'rest',
2537+
'X-Parse-Cloud-Context': 'key',
2538+
},
2539+
body: {
2540+
foo: 'bar',
2541+
},
2542+
});
2543+
try {
2544+
await req;
2545+
fail('Should have thrown error');
2546+
} catch (e) {
2547+
expect(e).toBeDefined();
2548+
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
2549+
}
2550+
expect(calledBefore).toBe(false);
2551+
expect(calledAfter).toBe(false);
2552+
});
2553+
2554+
it('should throw error if context header is string "1"', async () => {
2555+
let calledBefore = false;
2556+
let calledAfter = false;
2557+
Parse.Cloud.beforeSave('TestObject', () => {
2558+
calledBefore = true;
2559+
});
2560+
Parse.Cloud.afterSave('TestObject', () => {
2561+
calledAfter = true;
2562+
});
2563+
const req = request({
2564+
method: 'POST',
2565+
url: 'http://localhost:8378/1/classes/TestObject',
2566+
headers: {
2567+
'X-Parse-Application-Id': 'test',
2568+
'X-Parse-REST-API-Key': 'rest',
2569+
'X-Parse-Cloud-Context': '1',
2570+
},
2571+
body: {
2572+
foo: 'bar',
2573+
},
2574+
});
2575+
try {
2576+
await req;
2577+
fail('Should have thrown error');
2578+
} catch (e) {
2579+
expect(e).toBeDefined();
2580+
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
2581+
}
2582+
expect(calledBefore).toBe(false);
2583+
expect(calledAfter).toBe(false);
2584+
});
2585+
2586+
it('should expose context in beforeSave/afterSave via header', async () => {
2587+
let calledBefore = false;
2588+
let calledAfter = false;
2589+
Parse.Cloud.beforeSave('TestObject', req => {
2590+
expect(req.object.get('foo')).toEqual('bar');
2591+
expect(req.context.otherKey).toBe(1);
2592+
expect(req.context.key).toBe('value');
2593+
calledBefore = true;
2594+
});
2595+
Parse.Cloud.afterSave('TestObject', req => {
2596+
expect(req.object.get('foo')).toEqual('bar');
2597+
expect(req.context.otherKey).toBe(1);
2598+
expect(req.context.key).toBe('value');
2599+
calledAfter = true;
2600+
});
2601+
const req = request({
2602+
method: 'POST',
2603+
url: 'http://localhost:8378/1/classes/TestObject',
2604+
headers: {
2605+
'X-Parse-Application-Id': 'test',
2606+
'X-Parse-REST-API-Key': 'rest',
2607+
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
2608+
},
2609+
body: {
2610+
foo: 'bar',
2611+
},
2612+
});
2613+
await req;
2614+
expect(calledBefore).toBe(true);
2615+
expect(calledAfter).toBe(true);
2616+
});
2617+
2618+
it('should override header context with body context in beforeSave/afterSave', async () => {
2619+
let calledBefore = false;
2620+
let calledAfter = false;
2621+
Parse.Cloud.beforeSave('TestObject', req => {
2622+
expect(req.object.get('foo')).toEqual('bar');
2623+
expect(req.context.otherKey).toBe(10);
2624+
expect(req.context.key).toBe('hello');
2625+
calledBefore = true;
2626+
});
2627+
Parse.Cloud.afterSave('TestObject', req => {
2628+
expect(req.object.get('foo')).toEqual('bar');
2629+
expect(req.context.otherKey).toBe(10);
2630+
expect(req.context.key).toBe('hello');
2631+
calledAfter = true;
2632+
});
2633+
const req = request({
2634+
method: 'POST',
2635+
url: 'http://localhost:8378/1/classes/TestObject',
2636+
headers: {
2637+
'X-Parse-REST-API-Key': 'rest',
2638+
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
2639+
},
2640+
body: {
2641+
foo: 'bar',
2642+
_ApplicationId: 'test',
2643+
_context: '{"key":"hello","otherKey":10}',
2644+
},
2645+
});
2646+
await req;
2647+
expect(calledBefore).toBe(true);
2648+
expect(calledAfter).toBe(true);
2649+
});
2650+
2651+
it('should throw error if context body is malformed', async () => {
2652+
let calledBefore = false;
2653+
let calledAfter = false;
2654+
Parse.Cloud.beforeSave('TestObject', () => {
2655+
calledBefore = true;
2656+
});
2657+
Parse.Cloud.afterSave('TestObject', () => {
2658+
calledAfter = true;
2659+
});
2660+
const req = request({
2661+
method: 'POST',
2662+
url: 'http://localhost:8378/1/classes/TestObject',
2663+
headers: {
2664+
'X-Parse-REST-API-Key': 'rest',
2665+
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
2666+
},
2667+
body: {
2668+
foo: 'bar',
2669+
_ApplicationId: 'test',
2670+
_context: 'key',
2671+
},
2672+
});
2673+
try {
2674+
await req;
2675+
fail('Should have thrown error');
2676+
} catch (e) {
2677+
expect(e).toBeDefined();
2678+
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
2679+
}
2680+
expect(calledBefore).toBe(false);
2681+
expect(calledAfter).toBe(false);
2682+
});
2683+
2684+
it('should throw error if context body is string "true"', async () => {
2685+
let calledBefore = false;
2686+
let calledAfter = false;
2687+
Parse.Cloud.beforeSave('TestObject', () => {
2688+
calledBefore = true;
2689+
});
2690+
Parse.Cloud.afterSave('TestObject', () => {
2691+
calledAfter = true;
2692+
});
2693+
const req = request({
2694+
method: 'POST',
2695+
url: 'http://localhost:8378/1/classes/TestObject',
2696+
headers: {
2697+
'X-Parse-REST-API-Key': 'rest',
2698+
'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}',
2699+
},
2700+
body: {
2701+
foo: 'bar',
2702+
_ApplicationId: 'test',
2703+
_context: 'true',
2704+
},
2705+
});
2706+
try {
2707+
await req;
2708+
fail('Should have thrown error');
2709+
} catch (e) {
2710+
expect(e).toBeDefined();
2711+
expect(e.data.code).toEqual(Parse.Error.INVALID_JSON);
2712+
}
2713+
expect(calledBefore).toBe(false);
2714+
expect(calledAfter).toBe(false);
2715+
});
2716+
25222717
it('should expose context in before and afterSave', async () => {
25232718
let calledBefore = false;
25242719
let calledAfter = false;
@@ -2804,6 +2999,26 @@ describe('afterLogin hook', () => {
28042999
done();
28053000
});
28063001

3002+
it('context options should override _context object property when saving a new object', async () => {
3003+
Parse.Cloud.beforeSave('TestObject', req => {
3004+
expect(req.context.a).toEqual('a');
3005+
expect(req.context.hello).not.toBeDefined();
3006+
expect(req._context).not.toBeDefined();
3007+
expect(req.object._context).not.toBeDefined();
3008+
expect(req.object.context).not.toBeDefined();
3009+
});
3010+
Parse.Cloud.afterSave('TestObject', req => {
3011+
expect(req.context.a).toEqual('a');
3012+
expect(req.context.hello).not.toBeDefined();
3013+
expect(req._context).not.toBeDefined();
3014+
expect(req.object._context).not.toBeDefined();
3015+
expect(req.object.context).not.toBeDefined();
3016+
});
3017+
const obj = new TestObject();
3018+
obj.set('_context', { hello: 'world' });
3019+
await obj.save(null, { context: { a: 'a' } });
3020+
});
3021+
28073022
it('should have access to context when saving a new object', async () => {
28083023
Parse.Cloud.beforeSave('TestObject', req => {
28093024
expect(req.context.a).toEqual('a');

src/middlewares.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ const getMountForRequest = function (req) {
2525
export function handleParseHeaders(req, res, next) {
2626
var mount = getMountForRequest(req);
2727

28+
let context = {};
29+
if (req.get('X-Parse-Cloud-Context') != null) {
30+
try {
31+
context = JSON.parse(req.get('X-Parse-Cloud-Context'));
32+
if (Object.prototype.toString.call(context) !== '[object Object]') {
33+
throw 'Context is not an object';
34+
}
35+
} catch (e) {
36+
return malformedContext(req, res);
37+
}
38+
}
2839
var info = {
2940
appId: req.get('X-Parse-Application-Id'),
3041
sessionToken: req.get('X-Parse-Session-Token'),
@@ -35,7 +46,7 @@ export function handleParseHeaders(req, res, next) {
3546
dotNetKey: req.get('X-Parse-Windows-Key'),
3647
restAPIKey: req.get('X-Parse-REST-API-Key'),
3748
clientVersion: req.get('X-Parse-Client-Version'),
38-
context: {},
49+
context: context,
3950
};
4051

4152
var basicAuth = httpAuth(req);
@@ -105,8 +116,19 @@ export function handleParseHeaders(req, res, next) {
105116
info.masterKey = req.body._MasterKey;
106117
delete req.body._MasterKey;
107118
}
108-
if (req.body._context && req.body._context instanceof Object) {
109-
info.context = req.body._context;
119+
if (req.body._context) {
120+
if (req.body._context instanceof Object) {
121+
info.context = req.body._context;
122+
} else {
123+
try {
124+
info.context = JSON.parse(req.body._context);
125+
if (Object.prototype.toString.call(info.context) !== '[object Object]') {
126+
throw 'Context is not an object';
127+
}
128+
} catch (e) {
129+
return malformedContext(req, res);
130+
}
131+
}
110132
delete req.body._context;
111133
}
112134
if (req.body._ContentType) {
@@ -454,3 +476,8 @@ function invalidRequest(req, res) {
454476
res.status(403);
455477
res.end('{"error":"unauthorized"}');
456478
}
479+
480+
function malformedContext(req, res) {
481+
res.status(400);
482+
res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' });
483+
}

0 commit comments

Comments
 (0)