Files
parse-community-parse-server/spec/rest.spec.js
T

1674 lines
54 KiB
JavaScript

'use strict';
// These tests check the "create" / "update" functionality of the REST API.
const auth = require('../lib/Auth');
const Config = require('../lib/Config');
const Parse = require('parse/node').Parse;
const rest = require('../lib/rest');
const RestWrite = require('../lib/RestWrite');
const request = require('../lib/request');
let config;
let database;
describe('rest create', () => {
let loggerErrorSpy;
beforeEach(() => {
config = Config.get('test');
database = config.database;
const logger = require('../lib/logger').default;
loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
});
it('handles _id', done => {
rest
.create(config, auth.nobody(config), 'Foo', {})
.then(() => database.adapter.find('Foo', { fields: {} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
const obj = results[0];
expect(typeof obj.objectId).toEqual('string');
expect(obj.objectId.length).toEqual(10);
expect(obj._id).toBeUndefined();
done();
});
});
it('can use custom _id size', done => {
config.objectIdSize = 20;
rest
.create(config, auth.nobody(config), 'Foo', {})
.then(() => database.adapter.find('Foo', { fields: {} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
const obj = results[0];
expect(typeof obj.objectId).toEqual('string');
expect(obj.objectId.length).toEqual(20);
done();
});
});
it('should use objectId from client when allowCustomObjectId true', async () => {
config.allowCustomObjectId = true;
// use time as unique custom id for test reusability
const customId = `${Date.now()}`;
const obj = {
objectId: customId,
};
const {
status,
response: { objectId },
} = await rest.create(config, auth.nobody(config), 'MyClass', obj);
expect(status).toEqual(201);
expect(objectId).toEqual(customId);
});
it('should throw on invalid objectId when allowCustomObjectId true', () => {
config.allowCustomObjectId = true;
const objIdNull = {
objectId: null,
};
const objIdUndef = {
objectId: undefined,
};
const objIdEmpty = {
objectId: '',
};
const err = 'objectId must not be empty, null or undefined';
expect(() => rest.create(config, auth.nobody(config), 'MyClass', objIdEmpty)).toThrowError(err);
expect(() => rest.create(config, auth.nobody(config), 'MyClass', objIdNull)).toThrowError(err);
expect(() => rest.create(config, auth.nobody(config), 'MyClass', objIdUndef)).toThrowError(err);
});
it('should generate objectId when not set by client with allowCustomObjectId true', async () => {
config.allowCustomObjectId = true;
const {
status,
response: { objectId },
} = await rest.create(config, auth.nobody(config), 'MyClass', {});
expect(status).toEqual(201);
expect(objectId).toBeDefined();
});
it('is backwards compatible when _id size changes', done => {
rest
.create(config, auth.nobody(config), 'Foo', { size: 10 })
.then(() => {
config.objectIdSize = 20;
return rest.find(config, auth.nobody(config), 'Foo', { size: 10 });
})
.then(response => {
expect(response.results.length).toEqual(1);
expect(response.results[0].objectId.length).toEqual(10);
return rest.update(
config,
auth.nobody(config),
'Foo',
{ objectId: response.results[0].objectId },
{ update: 20 }
);
})
.then(() => {
return rest.find(config, auth.nobody(config), 'Foo', { size: 10 });
})
.then(response => {
expect(response.results.length).toEqual(1);
expect(response.results[0].objectId.length).toEqual(10);
expect(response.results[0].update).toEqual(20);
return rest.create(config, auth.nobody(config), 'Foo', { size: 20 });
})
.then(() => {
config.objectIdSize = 10;
return rest.find(config, auth.nobody(config), 'Foo', { size: 20 });
})
.then(response => {
expect(response.results.length).toEqual(1);
expect(response.results[0].objectId.length).toEqual(20);
done();
});
});
describe('with maintenance key', () => {
let req;
async function getObject(id) {
const res = await request({
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
method: 'GET',
url: `http://localhost:8378/1/classes/TestObject/${id}`,
});
return res.data;
}
beforeEach(() => {
req = {
headers: {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Maintenance-Key': 'testing',
},
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
};
});
it('allows createdAt', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
req.body = { createdAt };
const res = await request(req);
expect(res.data.createdAt).toEqual(createdAt.iso);
});
it('allows createdAt and updatedAt', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt };
const res = await request(req);
const obj = await getObject(res.data.objectId);
expect(obj.createdAt).toEqual(createdAt.iso);
expect(obj.updatedAt).toEqual(updatedAt.iso);
});
it('allows createdAt, updatedAt, and additional field', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt, testing: 123 };
const res = await request(req);
const obj = await getObject(res.data.objectId);
expect(obj.createdAt).toEqual(createdAt.iso);
expect(obj.updatedAt).toEqual(updatedAt.iso);
expect(obj.testing).toEqual(123);
});
it('cannot set updatedAt dated before createdAt', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt };
try {
await request(req);
fail();
} catch (err) {
expect(err.data.code).toEqual(Parse.Error.VALIDATION_ERROR);
}
});
it('cannot set updatedAt without createdAt', async () => {
const updatedAt = { __type: 'Date', iso: '2018-12-01T00:00:00.000Z' };
req.body = { updatedAt };
const res = await request(req);
const obj = await getObject(res.data.objectId);
expect(obj.updatedAt).not.toEqual(updatedAt.iso);
});
it('handles bad types for createdAt and updatedAt', async () => {
const createdAt = 12345;
const updatedAt = true;
req.body = { createdAt, updatedAt };
try {
await request(req);
fail();
} catch (err) {
expect(err.data.code).toEqual(Parse.Error.INCORRECT_TYPE);
}
});
it('cannot set createdAt or updatedAt without maintenance key', async () => {
const createdAt = { __type: 'Date', iso: '2019-01-01T00:00:00.000Z' };
const updatedAt = { __type: 'Date', iso: '2019-02-01T00:00:00.000Z' };
req.body = { createdAt, updatedAt };
delete req.headers['X-Parse-Maintenance-Key'];
const res = await request(req);
expect(res.data.createdAt).not.toEqual(createdAt.iso);
expect(res.data.updatedAt).not.toEqual(updatedAt.iso);
});
});
it_id('6c30306f-328c-47f2-88a7-2deffaee997f')(it)('handles array, object, date', done => {
const now = new Date();
const obj = {
array: [1, 2, 3],
object: { foo: 'bar' },
date: Parse._encode(now),
};
rest
.create(config, auth.nobody(config), 'MyClass', obj)
.then(() =>
database.adapter.find(
'MyClass',
{
fields: {
array: { type: 'Array' },
object: { type: 'Object' },
date: { type: 'Date' },
},
},
{},
{}
)
)
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(mob.array instanceof Array).toBe(true);
expect(typeof mob.object).toBe('object');
expect(mob.date.__type).toBe('Date');
expect(new Date(mob.date.iso).getTime()).toBe(now.getTime());
done();
});
});
it('handles object and subdocument', done => {
const obj = { subdoc: { foo: 'bar', wu: 'tan' } };
Parse.Cloud.beforeSave('MyClass', function () {
// this beforeSave trigger should do nothing but can mess with the object
});
rest
.create(config, auth.nobody(config), 'MyClass', obj)
.then(() => database.adapter.find('MyClass', { fields: {} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('tan');
expect(typeof mob.objectId).toEqual('string');
const obj = { 'subdoc.wu': 'clan' };
return rest.update(config, auth.nobody(config), 'MyClass', { objectId: mob.objectId }, obj);
})
.then(() => database.adapter.find('MyClass', { fields: {} }, {}, {}))
.then(results => {
expect(results.length).toEqual(1);
const mob = results[0];
expect(typeof mob.subdoc).toBe('object');
expect(mob.subdoc.foo).toBe('bar');
expect(mob.subdoc.wu).toBe('clan');
done();
})
.catch(done.fail);
});
it('handles create on non-existent class when disabled client class creation', done => {
const customConfig = Object.assign({}, config, {
allowClientClassCreation: false,
});
loggerErrorSpy.calls.reset();
rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}).then(
() => {
fail('Should throw an error');
done();
},
err => {
expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN);
expect(err.message).toEqual('Permission denied');
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining('This user is not allowed to access ' + 'non-existent class: ClientClassCreation'));
done();
}
);
});
it('handles create on existent class when disabled client class creation', async () => {
const customConfig = Object.assign({}, config, {
allowClientClassCreation: false,
});
const schema = await config.database.loadSchema();
const actualSchema = await schema.addClassIfNotExists('ClientClassCreation', {});
expect(actualSchema.className).toEqual('ClientClassCreation');
await schema.reloadData({ clearCache: true });
// Should not throw
await rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {});
});
it('handles user signup', done => {
const user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
rest.create(config, auth.nobody(config), '_User', user).then(r => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
done();
});
});
it('handles anonymous user signup', done => {
const data1 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001',
},
},
};
const data2 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000002',
},
},
};
let username1;
rest
.create(config, auth.nobody(config), '_User', data1)
.then(r => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
expect(typeof r.response.username).toEqual('string');
return rest.create(config, auth.nobody(config), '_User', data1);
})
.then(r => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.username).toEqual('string');
expect(typeof r.response.updatedAt).toEqual('string');
username1 = r.response.username;
return rest.create(config, auth.nobody(config), '_User', data2);
})
.then(r => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.create(config, auth.nobody(config), '_User', data2);
})
.then(r => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.username).toEqual('string');
expect(typeof r.response.updatedAt).toEqual('string');
expect(r.response.username).not.toEqual(username1);
done();
});
});
it('handles anonymous user signup and upgrade to new user', done => {
const data1 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001',
},
},
};
const updatedData = {
authData: { anonymous: null },
username: 'hello',
password: 'world',
};
let objectId;
rest
.create(config, auth.nobody(config), '_User', data1)
.then(r => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
objectId = r.response.objectId;
return auth.getAuthForSessionToken({
config,
sessionToken: r.response.sessionToken,
});
})
.then(sessionAuth => {
return rest.update(config, sessionAuth, '_User', { objectId }, updatedData);
})
.then(() => {
return Parse.User.logOut().then(() => {
return Parse.User.logIn('hello', 'world');
});
})
.then(r => {
expect(r.id).toEqual(objectId);
expect(r.get('username')).toEqual('hello');
done();
})
.catch(err => {
jfail(err);
done();
});
});
it('handles no anonymous users config', done => {
const NoAnnonConfig = Object.assign({}, config);
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false);
const data1 = {
authData: {
anonymous: {
id: '00000000-0000-0000-0000-000000000001',
},
},
};
rest.create(NoAnnonConfig, auth.nobody(NoAnnonConfig), '_User', data1).then(
() => {
fail('Should throw an error');
done();
},
err => {
expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE);
expect(err.message).toEqual('This authentication method is unsupported.');
NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true);
done();
}
);
});
it('test facebook signup and login', done => {
const data = {
authData: {
facebook: {
id: '8675309',
access_token: 'jenny',
},
},
};
let newUserSignedUpByFacebookObjectId;
rest
.create(config, auth.nobody(config), '_User', data)
.then(r => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
newUserSignedUpByFacebookObjectId = r.response.objectId;
return rest.create(config, auth.nobody(config), '_User', data);
})
.then(r => {
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.username).toEqual('string');
expect(typeof r.response.updatedAt).toEqual('string');
expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId);
return rest.find(config, auth.master(config), '_Session', {
sessionToken: r.response.sessionToken,
});
})
.then(response => {
expect(response.results.length).toEqual(1);
const output = response.results[0];
expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
done();
})
.catch(err => {
jfail(err);
done();
});
});
it('stores pointers', done => {
const obj = {
foo: 'bar',
aPointer: {
__type: 'Pointer',
className: 'JustThePointer',
objectId: 'qwerty1234', // make it 10 chars to match PG storage
},
};
rest
.create(config, auth.nobody(config), 'APointerDarkly', obj)
.then(() =>
database.adapter.find(
'APointerDarkly',
{
fields: {
foo: { type: 'String' },
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
},
},
{},
{}
)
)
.then(results => {
expect(results.length).toEqual(1);
const output = results[0];
expect(typeof output.foo).toEqual('string');
expect(typeof output._p_aPointer).toEqual('undefined');
expect(output._p_aPointer).toBeUndefined();
expect(output.aPointer).toEqual({
__type: 'Pointer',
className: 'JustThePointer',
objectId: 'qwerty1234',
});
done();
});
});
it('stores pointers to objectIds larger than 10 characters', done => {
const obj = {
foo: 'bar',
aPointer: {
__type: 'Pointer',
className: 'JustThePointer',
objectId: '49F62F92-9B56-46E7-A3D4-BBD14C52F666',
},
};
rest
.create(config, auth.nobody(config), 'APointerDarkly', obj)
.then(() =>
database.adapter.find(
'APointerDarkly',
{
fields: {
foo: { type: 'String' },
aPointer: { type: 'Pointer', targetClass: 'JustThePointer' },
},
},
{},
{}
)
)
.then(results => {
expect(results.length).toEqual(1);
const output = results[0];
expect(typeof output.foo).toEqual('string');
expect(typeof output._p_aPointer).toEqual('undefined');
expect(output._p_aPointer).toBeUndefined();
expect(output.aPointer).toEqual({
__type: 'Pointer',
className: 'JustThePointer',
objectId: '49F62F92-9B56-46E7-A3D4-BBD14C52F666',
});
done();
});
});
it('cannot set objectId', done => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
body: JSON.stringify({
foo: 'bar',
objectId: 'hello',
}),
}).then(fail, response => {
const b = response.data;
expect(b.code).toEqual(105);
expect(b.error).toEqual('objectId is an invalid field name.');
done();
});
});
it('cannot set id', done => {
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
request({
headers: headers,
method: 'POST',
url: 'http://localhost:8378/1/classes/TestObject',
body: JSON.stringify({
foo: 'bar',
id: 'hello',
}),
}).then(fail, response => {
const b = response.data;
expect(b.code).toEqual(105);
expect(b.error).toEqual('id is an invalid field name.');
done();
});
});
it('test default session length', done => {
const user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
const now = new Date();
rest
.create(config, auth.nobody(config), '_User', user)
.then(r => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config), '_Session', {
sessionToken: r.response.sessionToken,
});
})
.then(r => {
expect(r.results.length).toEqual(1);
const session = r.results[0];
const actual = new Date(session.expiresAt.iso);
const expected = new Date(now.getTime() + 1000 * 3600 * 24 * 365);
expect(Math.abs(actual - expected) <= jasmine.DEFAULT_TIMEOUT_INTERVAL).toEqual(true);
done();
});
});
it('test specified session length', done => {
const user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
const sessionLength = 3600, // 1 Hour ahead
now = new Date(); // For reference later
config.sessionLength = sessionLength;
rest
.create(config, auth.nobody(config), '_User', user)
.then(r => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config), '_Session', {
sessionToken: r.response.sessionToken,
});
})
.then(r => {
expect(r.results.length).toEqual(1);
const session = r.results[0];
const actual = new Date(session.expiresAt.iso);
const expected = new Date(now.getTime() + sessionLength * 1000);
expect(Math.abs(actual - expected) <= jasmine.DEFAULT_TIMEOUT_INTERVAL).toEqual(true);
done();
})
.catch(err => {
jfail(err);
done();
});
});
it('can create a session with no expiration', done => {
const user = {
username: 'asdf',
password: 'zxcv',
foo: 'bar',
};
config.expireInactiveSessions = false;
rest
.create(config, auth.nobody(config), '_User', user)
.then(r => {
expect(Object.keys(r.response).length).toEqual(3);
expect(typeof r.response.objectId).toEqual('string');
expect(typeof r.response.createdAt).toEqual('string');
expect(typeof r.response.sessionToken).toEqual('string');
return rest.find(config, auth.master(config), '_Session', {
sessionToken: r.response.sessionToken,
});
})
.then(r => {
expect(r.results.length).toEqual(1);
const session = r.results[0];
expect(session.expiresAt).toBeUndefined();
done();
})
.catch(err => {
console.error(err);
fail(err);
done();
});
});
it('can create object in volatileClasses if masterKey', done => {
rest
.create(config, auth.master(config), '_PushStatus', {})
.then(r => {
expect(r.response.objectId.length).toBe(10);
})
.then(() => {
rest.create(config, auth.master(config), '_JobStatus', {}).then(r => {
expect(r.response.objectId.length).toBe(10);
done();
});
});
});
it('cannot create object in volatileClasses if not masterKey', done => {
Promise.resolve()
.then(() => {
return rest.create(config, auth.nobody(config), '_PushStatus', {});
})
.catch(error => {
expect(error.code).toEqual(119);
done();
});
});
it('cannot get object in volatileClasses if not masterKey through pointer', async () => {
loggerErrorSpy.calls.reset();
const masterKeyOnlyClassObject = new Parse.Object('_PushStatus');
await masterKeyOnlyClassObject.save(null, { useMasterKey: true });
const obj2 = new Parse.Object('TestObject');
// Anyone is can basically create a pointer to any object
// or some developers can use master key in some hook to link
// private objects to standard objects
obj2.set('pointer', masterKeyOnlyClassObject);
await obj2.save();
const query = new Parse.Query('TestObject');
query.include('pointer');
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
'Permission denied'
);
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _PushStatus collection."));
});
it_id('3ce563bf-93aa-4d0b-9af9-c5fb246ac9fc')(it)('cannot get object in _GlobalConfig if not masterKey through pointer', async () => {
loggerErrorSpy.calls.reset();
await Parse.Config.save({ privateData: 'secret' }, { privateData: true });
const obj2 = new Parse.Object('TestObject');
obj2.set('globalConfigPointer', {
__type: 'Pointer',
className: '_GlobalConfig',
objectId: 1,
});
await obj2.save();
const query = new Parse.Query('TestObject');
query.include('globalConfigPointer');
await expectAsync(query.get(obj2.id)).toBeRejectedWithError(
'Permission denied'
);
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Clients aren't allowed to perform the get operation on the _GlobalConfig collection."));
});
it('should require master key for all volatile classes', () => {
// This test guards against drift between volatileClasses (SchemaController.js)
// and classesWithMasterOnlyAccess (SharedRest.js). If a new volatile class is
// added, it must also be added to classesWithMasterOnlyAccess and this test.
const volatileClasses = [
'_JobStatus',
'_PushStatus',
'_Hooks',
'_GlobalConfig',
'_GraphQLConfig',
'_JobSchedule',
'_Audience',
'_Idempotency',
];
for (const className of volatileClasses) {
expect(() =>
rest.create(config, auth.nobody(config), className, {})
).toThrowMatching(
e => e.code === Parse.Error.OPERATION_FORBIDDEN,
`Expected ${className} to require master key`
);
}
});
it('cannot find objects in _GraphQLConfig without masterKey', async () => {
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
await expectAsync(
rest.find(config, auth.nobody(config), '_GraphQLConfig', {})
).toBeRejectedWith(
jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN })
);
});
it('cannot update object in _GraphQLConfig without masterKey', async () => {
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
expect(() =>
rest.update(config, auth.nobody(config), '_GraphQLConfig', '1', {
config: { enabledForClasses: [] },
})
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
});
it('cannot delete object in _GraphQLConfig without masterKey', async () => {
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
expect(() =>
rest.del(config, auth.nobody(config), '_GraphQLConfig', '1')
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
});
it('can perform operations on _GraphQLConfig with masterKey', async () => {
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
const found = await rest.find(config, auth.master(config), '_GraphQLConfig', {});
expect(found.results.length).toBeGreaterThan(0);
await rest.del(config, auth.master(config), '_GraphQLConfig', '1');
const afterDelete = await rest.find(config, auth.master(config), '_GraphQLConfig', {});
expect(afterDelete.results.length).toBe(0);
});
it('cannot create object in _Audience without masterKey', () => {
expect(() =>
rest.create(config, auth.nobody(config), '_Audience', {
name: 'test',
query: '{}',
})
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
});
it('cannot find objects in _Audience without masterKey', async () => {
await expectAsync(
rest.find(config, auth.nobody(config), '_Audience', {})
).toBeRejectedWith(
jasmine.objectContaining({ code: Parse.Error.OPERATION_FORBIDDEN })
);
});
it('cannot update object in _Audience without masterKey', async () => {
const obj = await rest.create(config, auth.master(config), '_Audience', {
name: 'test',
query: '{}',
});
expect(() =>
rest.update(config, auth.nobody(config), '_Audience', obj.response.objectId, {
name: 'updated',
})
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
});
it('cannot delete object in _Audience without masterKey', async () => {
const obj = await rest.create(config, auth.master(config), '_Audience', {
name: 'test',
query: '{}',
});
expect(() =>
rest.del(config, auth.nobody(config), '_Audience', obj.response.objectId)
).toThrowMatching(e => e.code === Parse.Error.OPERATION_FORBIDDEN);
});
it('can perform CRUD on _Audience with masterKey', async () => {
const obj = await rest.create(config, auth.master(config), '_Audience', {
name: 'test',
query: '{}',
});
expect(obj.response.objectId).toBeDefined();
const found = await rest.find(config, auth.master(config), '_Audience', {});
expect(found.results.length).toBeGreaterThan(0);
await rest.del(config, auth.master(config), '_Audience', obj.response.objectId);
const afterDelete = await rest.find(config, auth.master(config), '_Audience', {});
expect(afterDelete.results.length).toBe(0);
});
it('cannot access _GraphQLConfig via class route without masterKey', async () => {
await config.parseGraphQLController.updateGraphQLConfig({ enabledForClasses: ['_User'] });
try {
await request({
url: 'http://localhost:8378/1/classes/_GraphQLConfig',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
});
fail('should have thrown');
} catch (e) {
expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
}
});
it('cannot access _Audience via class route without masterKey', async () => {
await rest.create(config, auth.master(config), '_Audience', {
name: 'test',
query: '{}',
});
try {
await request({
url: 'http://localhost:8378/1/classes/_Audience',
json: true,
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
});
fail('should have thrown');
} catch (e) {
expect(e.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
}
});
it('locks down session', done => {
let currentUser;
Parse.User.signUp('foo', 'bar')
.then(user => {
currentUser = user;
const sessionToken = user.getSessionToken();
const headers = {
'Content-Type': 'application/json',
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': sessionToken,
};
let sessionId;
return request({
headers: headers,
url: 'http://localhost:8378/1/sessions/me',
})
.then(response => {
sessionId = response.data.objectId;
return request({
headers,
method: 'PUT',
url: 'http://localhost:8378/1/sessions/' + sessionId,
body: {
installationId: 'yolo',
},
});
})
.then(done.fail, res => {
expect(res.status).toBe(400);
expect(res.data.code).toBe(105);
return request({
headers,
method: 'PUT',
url: 'http://localhost:8378/1/sessions/' + sessionId,
body: {
sessionToken: 'yolo',
},
});
})
.then(done.fail, res => {
expect(res.status).toBe(400);
expect(res.data.code).toBe(105);
return Parse.User.signUp('other', 'user');
})
.then(otherUser => {
const user = new Parse.User();
user.id = otherUser.id;
return request({
headers,
method: 'PUT',
url: 'http://localhost:8378/1/sessions/' + sessionId,
body: {
user: Parse._encode(user),
},
});
})
.then(done.fail, res => {
expect(res.status).toBe(400);
expect(res.data.code).toBe(105);
const user = new Parse.User();
user.id = currentUser.id;
return request({
headers,
method: 'PUT',
url: 'http://localhost:8378/1/sessions/' + sessionId,
body: {
user: Parse._encode(user),
},
});
})
.then(done)
.catch(done.fail);
})
.catch(done.fail);
});
it('sets current user in new sessions', done => {
let currentUser;
Parse.User.signUp('foo', 'bar')
.then(user => {
currentUser = user;
const sessionToken = user.getSessionToken();
const headers = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'X-Parse-Session-Token': sessionToken,
'Content-Type': 'application/json',
};
return request({
headers,
method: 'POST',
url: 'http://localhost:8378/1/sessions',
body: {
user: { __type: 'Pointer', className: '_User', objectId: 'fakeId' },
},
});
})
.then(response => {
if (response.data.user.objectId === currentUser.id) {
return done();
} else {
return done.fail();
}
})
.catch(done.fail);
});
});
describe('rest update', () => {
it('ignores createdAt', done => {
const config = Config.get('test');
const nobody = auth.nobody(config);
const className = 'Foo';
const newCreatedAt = new Date('1970-01-01T00:00:00.000Z');
rest
.create(config, nobody, className, {})
.then(res => {
const objectId = res.response.objectId;
const restObject = {
createdAt: { __type: 'Date', iso: newCreatedAt }, // should be ignored
};
return rest.update(config, nobody, className, { objectId }, restObject).then(() => {
const restWhere = {
objectId: objectId,
};
return rest.find(config, nobody, className, restWhere, {});
});
})
.then(res2 => {
const updatedObject = res2.results[0];
expect(new Date(updatedObject.createdAt)).not.toEqual(newCreatedAt);
done();
})
.then(done)
.catch(done.fail);
});
});
describe('_Join table security', () => {
let config;
beforeEach(() => {
config = Config.get('test');
});
it('cannot create object in _Join table without masterKey', () => {
expect(() =>
rest.create(config, auth.nobody(config), '_Join:users:_Role', {
relatedId: 'someUserId',
owningId: 'someRoleId',
})
).toThrowError(/Permission denied/);
});
it('cannot find objects in _Join table without masterKey', async () => {
await expectAsync(
rest.find(config, auth.nobody(config), '_Join:users:_Role', {})
).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});
it('cannot update object in _Join table without masterKey', () => {
expect(() =>
rest.update(config, auth.nobody(config), '_Join:users:_Role', { relatedId: 'someUserId' }, { owningId: 'newRoleId' })
).toThrowError(/Permission denied/);
});
it('cannot delete object in _Join table without masterKey', () => {
expect(() =>
rest.del(config, auth.nobody(config), '_Join:users:_Role', 'someObjectId')
).toThrowError(/Permission denied/);
});
it('cannot get object in _Join table without masterKey', async () => {
await expectAsync(
rest.get(config, auth.nobody(config), '_Join:users:_Role', 'someObjectId')
).toBeRejectedWith(
jasmine.objectContaining({
code: Parse.Error.OPERATION_FORBIDDEN,
})
);
});
it('can find objects in _Join table with masterKey', async () => {
await expectAsync(
rest.find(config, auth.master(config), '_Join:users:_Role', {})
).toBeResolved();
});
it('can find objects in _Join table with maintenance key', async () => {
await expectAsync(
rest.find(config, auth.maintenance(config), '_Join:users:_Role', {})
).toBeResolved();
});
it('legitimate relation operations still work', async () => {
const role = new Parse.Role('admin', new Parse.ACL());
const user = await Parse.User.signUp('testuser', 'password123');
role.getUsers().add(user);
await role.save(null, { useMasterKey: true });
const result = await rest.find(config, auth.master(config), '_Join:users:_Role', {});
expect(result.results.length).toBe(1);
});
it('blocks _Join table access for any relation, not just _Role', () => {
expect(() =>
rest.create(config, auth.nobody(config), '_Join:viewers:ConfidentialDoc', {
relatedId: 'someUserId',
owningId: 'someDocId',
})
).toThrowError(/Permission denied/);
});
it('cannot escalate role via direct _Join table write', async () => {
const role = new Parse.Role('superadmin', new Parse.ACL());
await role.save(null, { useMasterKey: true });
const user = await Parse.User.signUp('attacker', 'password123');
const sessionToken = user.getSessionToken();
const userAuth = await auth.getAuthForSessionToken({
config,
sessionToken,
});
expect(() =>
rest.create(config, userAuth, '_Join:users:_Role', {
relatedId: user.id,
owningId: role.id,
})
).toThrowError(/Permission denied/);
});
it('cannot write to _Join table with read-only masterKey', () => {
expect(() =>
rest.create(config, auth.readOnly(config), '_Join:users:_Role', {
relatedId: 'someUserId',
owningId: 'someRoleId',
})
).toThrowError(/Permission denied/);
});
it('can read _Join table with read-only masterKey', async () => {
await expectAsync(
rest.find(config, auth.readOnly(config), '_Join:users:_Role', {})
).toBeResolved();
});
});
describe('read-only masterKey', () => {
let loggerErrorSpy;
let logger;
beforeEach(() => {
logger = require('../lib/logger').default;
loggerErrorSpy = spyOn(logger, 'error').and.callThrough();
});
it('properly throws on rest.create, rest.update and rest.del', () => {
loggerErrorSpy.calls.reset();
const config = Config.get('test');
const readOnly = auth.readOnly(config);
expect(() => {
rest.create(config, readOnly, 'AnObject', {});
}).toThrow(
new Parse.Error(
Parse.Error.OPERATION_FORBIDDEN,
'Permission denied'
)
);
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation."));
expect(() => {
rest.update(config, readOnly, 'AnObject', {});
}).toThrow();
expect(() => {
rest.del(config, readOnly, 'AnObject', {});
}).toThrow();
});
it('properly blocks writes', async () => {
await reconfigureServer({
readOnlyMasterKey: 'yolo-read-only',
});
// Need to be re required because reconfigureServer resets the logger
const logger2 = require('../lib/logger').default;
loggerErrorSpy = spyOn(logger2, 'error').and.callThrough();
try {
await request({
url: `${Parse.serverURL}/classes/MyYolo`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'yolo-read-only',
'Content-Type': 'application/json',
},
body: { foo: 'bar' },
});
fail();
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe(
'Permission denied'
);
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to perform the create operation."));
}
await reconfigureServer();
});
it('should throw when masterKey and readOnlyMasterKey are the same', async () => {
try {
await reconfigureServer({
masterKey: 'yolo',
readOnlyMasterKey: 'yolo',
});
fail();
} catch (err) {
expect(err).toEqual(new Error('masterKey and readOnlyMasterKey should be different'));
}
await reconfigureServer();
});
it('should throw when masterKey and maintenanceKey are the same', async () => {
await expectAsync(
reconfigureServer({
masterKey: 'yolo',
maintenanceKey: 'yolo',
})
).toBeRejectedWith(new Error('masterKey and maintenanceKey should be different'));
});
it('should throw when trying to create RestWrite', () => {
loggerErrorSpy.calls.reset();
const config = Config.get('test');
expect(() => {
new RestWrite(config, auth.readOnly(config));
}).toThrow(
new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied')
);
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("Cannot perform a write operation when using readOnlyMasterKey"));
});
it('should throw when trying to create schema', done => {
loggerErrorSpy.calls.reset();
request({
method: 'POST',
url: `${Parse.serverURL}/schemas`,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
json: {},
})
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema."));
done();
});
});
it('should throw when trying to create schema with a name', done => {
loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/schemas/MyClass`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
json: {},
})
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to create a schema."));
done();
});
});
it('should throw when trying to update schema', done => {
loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/schemas/MyClass`,
method: 'PUT',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
json: {},
})
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update a schema."));
done();
});
});
it('should throw when trying to delete schema', done => {
loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/schemas/MyClass`,
method: 'DELETE',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
json: {},
})
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to delete a schema."));
done();
});
});
it('should throw when trying to update the global config', done => {
loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/config`,
method: 'PUT',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
json: {},
})
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to update the config."));
done();
});
});
it('should throw when trying to send push', done => {
loggerErrorSpy.calls.reset();
request({
url: `${Parse.serverURL}/push`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
json: {},
})
.then(done.fail)
.catch(res => {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe(
'Permission denied'
);
expect(loggerErrorSpy).toHaveBeenCalledWith('Sanitized error:', jasmine.stringContaining("read-only masterKey isn't allowed to send push notifications."));
done();
});
});
it('should throw when trying to create a hook function', async () => {
loggerErrorSpy.calls.reset();
try {
await request({
url: `${Parse.serverURL}/hooks/functions`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
body: { functionName: 'readOnlyTest', url: 'https://example.com/hook' },
});
fail('should have thrown');
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
}
});
it('should throw when trying to create a hook trigger', async () => {
loggerErrorSpy.calls.reset();
try {
await request({
url: `${Parse.serverURL}/hooks/triggers`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
body: { className: 'MyClass', triggerName: 'beforeSave', url: 'https://example.com/hook' },
});
fail('should have thrown');
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
}
});
it('should throw when trying to update a hook function', async () => {
// First create the hook with the real master key
await request({
url: `${Parse.serverURL}/hooks/functions`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'Content-Type': 'application/json',
},
body: { functionName: 'readOnlyUpdateTest', url: 'https://example.com/hook' },
});
loggerErrorSpy.calls.reset();
try {
await request({
url: `${Parse.serverURL}/hooks/functions/readOnlyUpdateTest`,
method: 'PUT',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
body: { url: 'https://example.com/hacked' },
});
fail('should have thrown');
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
}
});
it('should throw when trying to delete a hook function', async () => {
// First create the hook with the real master key
await request({
url: `${Parse.serverURL}/hooks/functions`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'Content-Type': 'application/json',
},
body: { functionName: 'readOnlyDeleteTest', url: 'https://example.com/hook' },
});
loggerErrorSpy.calls.reset();
try {
await request({
url: `${Parse.serverURL}/hooks/functions/readOnlyDeleteTest`,
method: 'PUT',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
body: { __op: 'Delete' },
});
fail('should have thrown');
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
}
});
it('should throw when trying to run a job with readOnlyMasterKey', async () => {
Parse.Cloud.job('readOnlyTestJob', () => {});
loggerErrorSpy.calls.reset();
try {
await request({
url: `${Parse.serverURL}/jobs/readOnlyTestJob`,
method: 'POST',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
body: {},
});
fail('should have thrown');
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
}
});
it('should allow reading hooks with readOnlyMasterKey', async () => {
const res = await request({
url: `${Parse.serverURL}/hooks/functions`,
method: 'GET',
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
},
});
expect(Array.isArray(res.data)).toBe(true);
});
it('should throw when trying to delete a file with readOnlyMasterKey', async () => {
// Create a file with the real master key
const uploadRes = await request({
method: 'POST',
url: `${Parse.serverURL}/files/readonly-delete-test.txt`,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': Parse.masterKey,
'Content-Type': 'text/plain',
},
body: 'file content',
});
const filename = uploadRes.data.name;
expect(filename).toBeDefined();
// Attempt delete with readOnlyMasterKey — should be rejected
loggerErrorSpy.calls.reset();
try {
await request({
method: 'DELETE',
url: `${Parse.serverURL}/files/${filename}`,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
},
});
fail('should have thrown');
} catch (res) {
expect(res.status).toBe(403);
expect(res.data.error).toBe('Permission denied');
}
// Verify file still exists
const getRes = await request({ url: uploadRes.data.url });
expect(getRes.status).toBe(200);
});
it('should throw when trying to create a file with readOnlyMasterKey', async () => {
loggerErrorSpy.calls.reset();
try {
await request({
method: 'POST',
url: `${Parse.serverURL}/files/readonly-create-test.txt`,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'text/plain',
},
body: 'file content',
});
fail('should have thrown');
} catch (res) {
expect(res.status).toBe(403);
expect(res.data.error).toBe('Permission denied');
}
});
it('should throw when trying to loginAs with readOnlyMasterKey', async () => {
// Create a target user
await Parse.User.signUp('readonly-loginas-test', 'password123');
const userId = Parse.User.current().id;
await Parse.User.logOut();
// Attempt loginAs with readOnlyMasterKey — should be rejected
loggerErrorSpy.calls.reset();
try {
await request({
method: 'POST',
url: `${Parse.serverURL}/loginAs`,
headers: {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-Master-Key': 'read-only-test',
'Content-Type': 'application/json',
},
body: { userId },
});
fail('should have thrown');
} catch (res) {
expect(res.data.code).toBe(Parse.Error.OPERATION_FORBIDDEN);
expect(res.data.error).toBe('Permission denied');
}
});
});
describe('rest context', () => {
it('should support dependency injection on rest api', async () => {
const requestContextMiddleware = (req, res, next) => {
req.config.aCustomController = 'aCustomController';
next();
};
let called
await reconfigureServer({ requestContextMiddleware });
Parse.Cloud.beforeSave('_User', request => {
expect(request.config.aCustomController).toEqual('aCustomController');
called = true;
});
const user = new Parse.User();
user.setUsername('test');
user.setPassword('test');
await user.signUp();
expect(called).toBe(true);
});
});