diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 2778be21a5..b249df8a1c 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1122,4 +1122,282 @@ describe('schemas', () => { }) }); + function setPermissionsOnClass(className, permissions, doPut) { + let op = request.post; + if (doPut) + { + op = request.put; + } + return new Promise((resolve, reject) => { + op({ + url: 'http://localhost:8378/1/schemas/'+className, + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: permissions + } + }, (error, response, body) => { + if (error) { + return reject(error); + } + if (body.error) { + return reject(body); + } + return resolve(body); + }) + }); + } + + it('validate CLP 1', done => { + let user = new Parse.User(); + user.setUsername('user'); + user.setPassword('user'); + + let admin = new Parse.User(); + admin.setUsername('admin'); + admin.setPassword('admin'); + + let role = new Parse.Role('admin', new Parse.ACL()); + + setPermissionsOnClass('AClass', { + 'find': { + 'role:admin': true + } + }).then(() => { + return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); + }).then(()=> { + role.relation('users').add(admin); + return role.save(null, {useMasterKey: true}); + }).then(() => { + return Parse.User.logIn('user', 'user').then(() => { + let obj = new Parse.Object('AClass'); + return obj.save(); + }) + }).then(() => { + let query = new Parse.Query('AClass'); + return query.find().then((err) => { + expect(err.message).toEqual('Permission denied for this action.'); + fail('Use should hot be able to find!') + }, (err) => { + return Promise.resolve(); + }) + }).then(() => { + return Parse.User.logIn('admin', 'admin'); + }).then( () => { + let query = new Parse.Query('AClass'); + return query.find(); + }).then((results) => { + expect(results.length).toBe(1); + done(); + }, () => { + fail("should not fail!"); + done(); + }).catch( (err) => { + console.error(err); + done(); + }) + }); + + it('validate CLP 2', done => { + let user = new Parse.User(); + user.setUsername('user'); + user.setPassword('user'); + + let admin = new Parse.User(); + admin.setUsername('admin'); + admin.setPassword('admin'); + + let role = new Parse.Role('admin', new Parse.ACL()); + + setPermissionsOnClass('AClass', { + 'find': { + 'role:admin': true + } + }).then(() => { + return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); + }).then(()=> { + role.relation('users').add(admin); + return role.save(null, {useMasterKey: true}); + }).then(() => { + return Parse.User.logIn('user', 'user').then(() => { + let obj = new Parse.Object('AClass'); + return obj.save(); + }) + }).then(() => { + let query = new Parse.Query('AClass'); + return query.find().then((err) => { + expect(err.message).toEqual('Permission denied for this action.'); + fail('User should not be able to find!') + }, (err) => { + return Promise.resolve(); + }) + }).then(() => { + // let everyone see it now + return setPermissionsOnClass('AClass', { + 'find': { + 'role:admin': true, + '*': true + } + }, true); + }).then(() => { + let query = new Parse.Query('AClass'); + return query.find().then((result) => { + expect(result.length).toBe(1); + }, (err) => { + console.error(err); + expect(err.message).toEqual('Permission denied for this action.'); + fail('User should be able to find!') + done(); + }); + }).then(() => { + return Parse.User.logIn('admin', 'admin'); + }).then( () => { + let query = new Parse.Query('AClass'); + return query.find(); + }).then((results) => { + expect(results.length).toBe(1); + done(); + }, (err) => { + console.error(err); + fail("should not fail!"); + done(); + }).catch( (err) => { + console.error(err); + done(); + }) + }); + + it('validate CLP 3', done => { + let user = new Parse.User(); + user.setUsername('user'); + user.setPassword('user'); + + let admin = new Parse.User(); + admin.setUsername('admin'); + admin.setPassword('admin'); + + let role = new Parse.Role('admin', new Parse.ACL()); + + setPermissionsOnClass('AClass', { + 'find': { + 'role:admin': true + } + }).then(() => { + return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); + }).then(()=> { + role.relation('users').add(admin); + return role.save(null, {useMasterKey: true}); + }).then(() => { + return Parse.User.logIn('user', 'user').then(() => { + let obj = new Parse.Object('AClass'); + return obj.save(); + }) + }).then(() => { + let query = new Parse.Query('AClass'); + return query.find().then((err) => { + expect(err.message).toEqual('Permission denied for this action.'); + fail('User should not be able to find!') + }, (err) => { + return Promise.resolve(); + }) + }).then(() => { + // delete all CLP + return setPermissionsOnClass('AClass', null, true); + }).then(() => { + let query = new Parse.Query('AClass'); + return query.find().then((result) => { + expect(result.length).toBe(1); + }, (err) => { + console.error(err); + fail('User should be able to find!') + done(); + }); + }).then(() => { + return Parse.User.logIn('admin', 'admin'); + }).then( () => { + let query = new Parse.Query('AClass'); + return query.find(); + }).then((results) => { + expect(results.length).toBe(1); + done(); + }, (err) => { + console.error(err); + fail("should not fail!"); + done(); + }).catch( (err) => { + console.error(err); + done(); + }) + }); + + it('validate CLP 4', done => { + let user = new Parse.User(); + user.setUsername('user'); + user.setPassword('user'); + + let admin = new Parse.User(); + admin.setUsername('admin'); + admin.setPassword('admin'); + + let role = new Parse.Role('admin', new Parse.ACL()); + + setPermissionsOnClass('AClass', { + 'find': { + 'role:admin': true + } + }).then(() => { + return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); + }).then(()=> { + role.relation('users').add(admin); + return role.save(null, {useMasterKey: true}); + }).then(() => { + return Parse.User.logIn('user', 'user').then(() => { + let obj = new Parse.Object('AClass'); + return obj.save(); + }) + }).then(() => { + let query = new Parse.Query('AClass'); + return query.find().then((err) => { + expect(err.message).toEqual('Permission denied for this action.'); + fail('User should not be able to find!') + }, (err) => { + return Promise.resolve(); + }) + }).then(() => { + // borked CLP should not affec security + return setPermissionsOnClass('AClass', { + 'found': { + 'role:admin': true + } + }, true).then(() => { + fail("Should not be able to save a borked CLP"); + }, () => { + return Promise.resolve(); + }) + }).then(() => { + let query = new Parse.Query('AClass'); + return query.find().then((result) => { + fail('User should not be able to find!') + }, (err) => { + expect(err.message).toEqual('Permission denied for this action.'); + return Promise.resolve(); + }); + }).then(() => { + return Parse.User.logIn('admin', 'admin'); + }).then( () => { + let query = new Parse.Query('AClass'); + return query.find(); + }).then((results) => { + expect(results.length).toBe(1); + done(); + }, (err) => { + console.error(err); + fail("should not fail!"); + done(); + }).catch( (err) => { + console.error(err); + done(); + }) + }); + }); diff --git a/src/Schema.js b/src/Schema.js index 846a7490a4..2c9c57752e 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -262,19 +262,11 @@ class Schema { if (this.data[className]) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); } - if (classLevelPermissions) { - validateCLP(classLevelPermissions); - } - let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className); + let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions); if (!mongoObject.result) { return Promise.reject(mongoObject); } - - if (classLevelPermissions) { - mongoObject.result._metadata = mongoObject.result._metadata || {}; - mongoObject.result._metadata.class_permissions = classLevelPermissions; - } return this._collection.addSchema(className, mongoObject.result) .then(result => result.ops[0]) @@ -301,17 +293,12 @@ class Schema { } }); - validateCLP(classLevelPermissions); let newSchema = buildMergedSchemaObject(existingFields, submittedFields); - let mongoObject = mongoSchemaFromFieldsAndClassName(newSchema, className); + let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions); if (!mongoObject.result) { throw new Parse.Error(mongoObject.code, mongoObject.error); } - // set the class permissions - if (classLevelPermissions) { - mongoObject.result._metadata = mongoObject.result._metadata || {}; - mongoObject.result._metadata.class_permissions = classLevelPermissions; - } + // Finally we have checked to make sure the request is valid and we can start deleting fields. // Do all deletions first, then a single save to _SCHEMA collection to handle all additions. let deletePromises = []; @@ -333,6 +320,9 @@ class Schema { }); return Promise.all(promises); }) + .then(() => { + return this.setPermission(className, classLevelPermissions) + }) .then(() => { return mongoSchemaToSchemaAPIResponse(mongoObject.result) }); } @@ -383,6 +373,9 @@ class Schema { // Sets the Class-level permissions for a given className, which must exist. setPermissions(className, perms) { + if (typeof perms === 'undefined') { + return Promise.resolve(); + } validateCLP(perms); var update = { _metadata: { @@ -644,7 +637,7 @@ function load(collection) { // Returns { code, error } if invalid, or { result }, an object // suitable for inserting into _SCHEMA collection, otherwise -function mongoSchemaFromFieldsAndClassName(fields, className) { +function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) { if (!classNameIsValid(className)) { return { code: Parse.Error.INVALID_CLASS_NAME, @@ -697,6 +690,16 @@ function mongoSchemaFromFieldsAndClassName(fields, className) { error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.', }; } + + validateCLP(classLevelPermissions); + if (typeof classLevelPermissions !== 'undefined') { + mongoObject._metadata = mongoObject._metadata || {}; + if (!classLevelPermissions) { + delete mongoObject._metadata.class_permissions; + } else { + mongoObject._metadata.class_permissions = classLevelPermissions; + } + } return { result: mongoObject }; } @@ -886,7 +889,6 @@ module.exports = { load: load, classNameIsValid: classNameIsValid, invalidClassNameMessage: invalidClassNameMessage, - mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName, schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType, buildMergedSchemaObject: buildMergedSchemaObject, mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,