diff --git a/spec/PointerPermissions.spec.js b/spec/PointerPermissions.spec.js new file mode 100644 index 00000000..21320541 --- /dev/null +++ b/spec/PointerPermissions.spec.js @@ -0,0 +1,697 @@ +'use strict'; +var Schema = require('../src/Controllers/SchemaController'); + +var Config = require('../src/Config'); + +describe('Pointer Permissions', () => { + it('should work with find', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + let obj2 = new Parse.Object('AnObject'); + + Parse.Object.saveAll([user, user2]).then(() => { + obj.set('owner', user); + obj2.set('owner', user2); + return Parse.Object.saveAll([obj, obj2]); + }).then(() => { + return config.database.loadSchema().then((schema) => { + return schema.updateClass('AnObject', {}, {readUserFields: ['owner']}) + }); + }).then(() => { + return Parse.User.logIn('user1', 'password'); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.find(); + }).then((res) => { + expect(res.length).toBe(1); + expect(res[0].id).toBe(obj.id); + done(); + }).catch((err) => { + fail('Should not fail'); + done(); + }); + }); + + + it('should work with write', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + let obj2 = new Parse.Object('AnObject'); + + Parse.Object.saveAll([user, user2]).then(() => { + obj.set('owner', user); + obj.set('reader', user2); + obj2.set('owner', user2); + obj2.set('reader', user); + return Parse.Object.saveAll([obj, obj2]); + }).then(() => { + return config.database.loadSchema().then((schema) => { + return schema.updateClass('AnObject', {}, {writeUserFields: ['owner'], readUserFields: ['reader', 'owner']}); + }); + }).then(() => { + return Parse.User.logIn('user1', 'password'); + }).then(() => { + obj2.set('hello', 'world'); + return obj2.save(); + }).then((res) => { + fail('User should not be able to update obj2'); + }, (err) => { + // User 1 should not be able to update obj2 + expect(err.code).toBe(101); + return Promise.resolve(); + }).then(()=> { + obj.set('hello', 'world'); + return obj.save(); + }).then(() => { + return Parse.User.logIn('user2', 'password'); + }, (err) => { + fail('User should be able to update'); + return Promise.resolve(); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.find(); + }, (err) => { + fail('should login with user 2'); + }).then((res) => { + expect(res.length).toBe(2); + res.forEach((result) => { + if (result.id == obj.id) { + expect(result.get('hello')).toBe('world'); + } else { + expect(result.id).toBe(obj2.id); + } + }) + done(); + }, (err) =>  { + fail("failed"); + done(); + }) + }); + + it('should let a proper user find', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + let obj2 = new Parse.Object('AnObject'); + user.signUp().then(() => { + return user2.signUp() + }).then(() => { + Parse.User.logOut(); + }).then(() => { + obj.set('owner', user); + return Parse.Object.saveAll([obj, obj2]); + }).then(() => { + return config.database.loadSchema().then((schema) => { + return schema.updateClass('AnObject', {}, {find: {}, get:{}, readUserFields: ['owner']}) + }); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.find(); + }).then((res) => { + expect(res.length).toBe(0); + }).then(() => { + return Parse.User.logIn('user2', 'password'); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.find(); + }).then((res) => { + expect(res.length).toBe(0); + let q = new Parse.Query('AnObject'); + return q.get(obj.id); + }).then(() => { + fail('User 2 should not get the obj1 object'); + }, (err) => { + expect(err.code).toBe(101); + expect(err.message).toBe('Object not found.'); + return Promise.resolve(); + }).then(() => { + return Parse.User.logIn('user1', 'password'); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.find(); + }).then((res) => { + expect(res.length).toBe(1); + done(); + }).catch((err) => { + console.error(err); + fail('should not fail'); + done(); + }) + }); + + it('should not allow creating objects', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + user.save().then(() => { + return config.database.loadSchema().then((schema) => { + return schema.addClassIfNotExists('AnObject', {owner: {type:'Pointer', targetClass: '_User'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']}); + }); + }).then(() => { + return Parse.User.logIn('user1', 'password'); + }).then(() => { + obj.set('owner', user); + return obj.save(); + }).then(() => { + fail('should not succeed'); + done(); + }, (err) => { + expect(err.code).toBe(119); + done(); + }) + }); + + it('should handle multiple writeUserFields', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + Parse.Object.saveAll([user, user2]).then(() => { + obj.set('owner', user); + obj.set('otherOwner', user2); + return obj.save(); + }).then(() => { + return config.database.loadSchema().then((schema) => { + return schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']}); + }); + }).then(() => { + return Parse.User.logIn('user1', 'password'); + }).then(() => { + return obj.save({hello: 'fromUser1'}); + }).then(() => { + return Parse.User.logIn('user2', 'password'); + }).then(() => { + return obj.save({hello: 'fromUser2'}); + }).then(() => { + Parse.User.logOut(); + let q = new Parse.Query('AnObject'); + return q.first(); + }).then((result) => { + expect(result.get('hello')).toBe('fromUser2'); + done(); + }).catch(err => { + fail('should not fail'); + done(); + }) + }); + + it('should prevent creating pointer permission on missing field', (done) => { + let config = new Config(Parse.applicationId); + config.database.loadSchema().then((schema) => { + return schema.addClassIfNotExists('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']}); + }).then(() => { + fail('should not succeed'); + }).catch((err) => { + expect(err.code).toBe(107); + expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields"); + done(); + }) + }); + + it('should prevent creating pointer permission on bad field', (done) => { + let config = new Config(Parse.applicationId); + config.database.loadSchema().then((schema) => { + return schema.addClassIfNotExists('AnObject', {owner: {type: 'String'}}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']}); + }).then(() => { + fail('should not succeed'); + }).catch((err) => { + expect(err.code).toBe(107); + expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields"); + done(); + }) + }); + + it('should prevent creating pointer permission on bad field', (done) => { + let config = new Config(Parse.applicationId); + let object = new Parse.Object('AnObject'); + object.set('owner', 'value'); + object.save().then(() => { + return config.database.loadSchema(); + }).then((schema) => { + return schema.updateClass('AnObject', {}, {create: {}, writeUserFields: ['owner'], readUserFields: ['owner']}); + }).then(() => { + fail('should not succeed'); + }).catch((err) => { + expect(err.code).toBe(107); + expect(err.message).toBe("'owner' is not a valid column for class level pointer permissions writeUserFields"); + done(); + }) + }); + + it('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => { + /* + tests: + CLP: update open ({"*": true}) + PointerPerm: "owner" + ACL: logged in user has access + + The owner is another user than the ACL + */ + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + Parse.Object.saveAll([user, user2]).then(() => { + let ACL = new Parse.ACL(); + ACL.setReadAccess(user, true); + ACL.setWriteAccess(user, true); + obj.setACL(ACL); + obj.set('owner', user2); + return obj.save(); + }).then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {}, {update: {"*": true}, writeUserFields: ['owner']}); + }); + }).then(() => { + return Parse.User.logIn('user1', 'password'); + }).then(() => { + // user1 has ACL read/write but should be blocked by PP + return obj.save({key: 'value'}); + }).then(() => { + fail('Should not succeed saving'); + done(); + }, (err) => { + expect(err.code).toBe(101); + done(); + }); + }); + + it('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => { + /* + tests: + CLP: update open ({"*": true}) + PointerPerm: "owner" + ACL: logged in user has access + */ + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + Parse.Object.saveAll([user, user2]).then(() => { + let ACL = new Parse.ACL(); + ACL.setReadAccess(user, true); + ACL.setWriteAccess(user, true); + obj.setACL(ACL); + obj.set('owner', user2); + return obj.save(); + }).then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {}, {update: {"*": true}, writeUserFields: ['owner']}); + }); + }).then(() => { + return Parse.User.logIn('user2', 'password'); + }).then(() => { + // user1 has ACL read/write but should be blocked by ACL + return obj.save({key: 'value'}); + }).then(() => { + fail('Should not succeed saving'); + done(); + }, (err) => { + expect(err.code).toBe(101); + done(); + }); + }); + + it('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => { + /* + tests: + CLP: update open ({"*": true}) + PointerPerm: "owner" + ACL: logged in user has access + */ + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + Parse.Object.saveAll([user, user2]).then(() => { + let ACL = new Parse.ACL(); + ACL.setWriteAccess(user, true); + ACL.setWriteAccess(user2, true); + obj.setACL(ACL); + obj.set('owner', user2); + return obj.save(); + }).then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {}, {update: {"*": true}, writeUserFields: ['owner']}); + }); + }).then(() => { + return Parse.User.logIn('user2', 'password'); + }).then(() => { + // user1 has ACL read/write but should be blocked by ACL + return obj.save({key: 'value'}); + }).then((objAgain) => { + expect(objAgain.get('key')).toBe('value'); + done(); + }, (err) => { + fail('Should not fail saving'); + done(); + }); + }); + + it('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => { + /* + tests: + CLP: find/get open ({"*": true}) + PointerPerm: "owner" : read + ACL: logged in user has access + + The owner is another user than the ACL + */ + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + Parse.Object.saveAll([user, user2]).then(() => { + let ACL = new Parse.ACL(); + ACL.setReadAccess(user, true); + ACL.setWriteAccess(user, true); + obj.setACL(ACL); + obj.set('owner', user2); + return obj.save(); + }).then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']}); + }); + }).then(() => { + return Parse.User.logIn('user1', 'password'); + }).then(() => { + // user1 has ACL read/write but should be block + return obj.fetch(); + }).then(() => { + fail('Should not succeed saving'); + done(); + }, (err) => { + expect(err.code).toBe(101); + done(); + }); + }); + + it('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', (done) => { + /* + tests: + CLP: find/get open ({"*": true}) + PointerPerm: "owner" : read + ACL: logged in user has access + */ + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + Parse.Object.saveAll([user, user2]).then(() => { + let ACL = new Parse.ACL(); + ACL.setReadAccess(user, true); + ACL.setWriteAccess(user, true); + ACL.setReadAccess(user2, true); + ACL.setWriteAccess(user2, true); + obj.setACL(ACL); + obj.set('owner', user2); + return obj.save(); + }).then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']}); + }); + }).then(() => { + return Parse.User.logIn('user2', 'password'); + }).then(() => { + // user1 has ACL read/write but should be block + return obj.fetch(); + }).then((objAgain) => { + expect(objAgain.id).toBe(obj.id); + done(); + }, (err) => { + fail('Should not fail fetching'); + done(); + }); + }); + + it('tests CLP / Pointer Perms / ACL read (ACL locked)', (done) => { + /* + tests: + CLP: find/get open ({"*": true}) + PointerPerm: "owner" : read // proper owner + ACL: logged in user has not access + */ + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let user2 = new Parse.User(); + user.set({ + username: 'user1', + password: 'password' + }); + user2.set({ + username: 'user2', + password: 'password' + }); + let obj = new Parse.Object('AnObject'); + Parse.Object.saveAll([user, user2]).then(() => { + let ACL = new Parse.ACL(); + ACL.setReadAccess(user, true); + ACL.setWriteAccess(user, true); + obj.setACL(ACL); + obj.set('owner', user2); + return obj.save(); + }).then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {}, {find: {"*": true}, get: {"*": true}, readUserFields: ['owner']}); + }); + }).then(() => { + return Parse.User.logIn('user2', 'password'); + }).then(() => { + // user2 has ACL read/write but should be block by ACL + return obj.fetch(); + }).then(() => { + fail('Should not succeed saving'); + done(); + }, (err) => { + expect(err.code).toBe(101); + done(); + }); + }); + + it('should let master key find objects', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let object = new Parse.Object('AnObject'); + object.set('hello', 'world'); + return object.save().then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']}); + }); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.find(); + }).then(() => { + + }, (err) => { + expect(err.code).toBe(101); + return Promise.resolve(); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.find({useMasterKey: true}); + }).then((objects) => { + expect(objects.length).toBe(1); + done(); + }, (err) => { + fail('master key should find the object'); + done(); + }) + }); + + it('should let master key get objects', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let object = new Parse.Object('AnObject'); + object.set('hello', 'world'); + return object.save().then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {find: {}, get: {}, readUserFields: ['owner']}); + }); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.get(object.id); + }).then(() => { + + }, (err) => { + expect(err.code).toBe(101); + return Promise.resolve(); + }).then(() => { + let q = new Parse.Query('AnObject'); + return q.get(object.id, {useMasterKey: true}); + }).then((objectAgain) => { + expect(objectAgain).not.toBeUndefined(); + expect(objectAgain.id).toBe(object.id); + done(); + }, (err) => { + fail('master key should find the object'); + done(); + }) + }); + + + it('should let master key update objects', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let object = new Parse.Object('AnObject'); + object.set('hello', 'world'); + return object.save().then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {update: {}, writeUserFields: ['owner']}); + }); + }).then(() => { + return object.save({'hello': 'bar'}); + }).then(() => { + + }, (err) => { + expect(err.code).toBe(101); + return Promise.resolve(); + }).then(() => { + return object.save({'hello': 'baz'}, {useMasterKey: true}); + }).then((objectAgain) => { + expect(objectAgain.get('hello')).toBe('baz'); + done(); + }, (err) => { + fail('master key should save the object'); + done(); + }) + }); + + it('should let master key delete objects', (done) => { + let config = new Config(Parse.applicationId); + let user = new Parse.User(); + let object = new Parse.Object('AnObject'); + object.set('hello', 'world'); + return object.save().then(() => { + return config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.updateClass('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner']}); + }); + }).then(() => { + return object.destroy(); + }).then(() => { + + }, (err) => { + expect(err.code).toBe(101); + return Promise.resolve(); + }).then(() => { + return object.destroy({useMasterKey: true}); + }).then((objectAgain) => { + done(); + }, (err) => { + fail('master key should destroy the object'); + done(); + }) + }); + + it('should fail with invalid pointer perms', () => { + let config = new Config(Parse.applicationId); + config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: 'owner'}); + }).catch((err) => { + expect(err.code).toBe(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it('should fail with invalid pointer perms', () => { + let config = new Config(Parse.applicationId); + config.database.loadSchema().then((schema) => { + // Lock the update, and let only owner write + return schema.addClassIfNotExists('AnObject', {owner: {type: 'Pointer', targetClass: '_User'}}, {delete: {}, writeUserFields: ['owner', 'invalid']}); + }).catch((err) => { + expect(err.code).toBe(Parse.Error.INVALID_JSON); + done(); + }); + }) + +}); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 46c84174..850a5cad 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -151,6 +151,12 @@ DatabaseController.prototype.update = function(className, query, update, options .then(() => this.handleRelationUpdates(className, query.objectId, update)) .then(() => this.adapter.adaptiveCollection(className)) .then(collection => { + if (!isMaster) { + query = this.addPointerPermissions(schema, className, 'update', query, aclGroup); + } + if (!query) { + return Promise.resolve(); + } var mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation}); if (options.acl) { mongoWhere = this.transform.addWriteACL(mongoWhere, options.acl); @@ -291,6 +297,12 @@ DatabaseController.prototype.destroy = function(className, query, options = {}) }) .then(() => this.adapter.adaptiveCollection(className)) .then(collection => { + if (!isMaster) { + query = this.addPointerPermissions(schema, className, 'delete', query, aclGroup); + if (!query) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } + } let mongoWhere = this.transform.transformWhere(schema, className, query, {validate: !this.skipValidation}); if (options.acl) { mongoWhere = this.transform.addWriteACL(mongoWhere, options.acl); @@ -569,6 +581,9 @@ DatabaseController.prototype.find = function(className, query, options = {}) { let isMaster = !('acl' in options); let aclGroup = options.acl || []; let schema = null; + let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? + 'get' : + 'find'; return this.loadSchema().then(s => { schema = s; if (options.sort) { @@ -580,9 +595,6 @@ DatabaseController.prototype.find = function(className, query, options = {}) { } if (!isMaster) { - let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? - 'get' : - 'find'; return schema.validatePermission(className, aclGroup, op); } return Promise.resolve(); @@ -591,6 +603,17 @@ DatabaseController.prototype.find = function(className, query, options = {}) { .then(() => this.reduceInRelation(className, query, schema)) .then(() => this.adapter.adaptiveCollection(className)) .then(collection => { + if (!isMaster) { + query = this.addPointerPermissions(schema, className, op, query, aclGroup); + } + if (!query) { + if (op == 'get') { + return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.')); + } else { + return Promise.resolve([]); + } + } let mongoWhere = this.transform.transformWhere(schema, className, query); if (!isMaster) { mongoWhere = this.transform.addReadACL(mongoWhere, aclGroup); @@ -629,6 +652,42 @@ DatabaseController.prototype.deleteSchema = function(className) { }) } +DatabaseController.prototype.addPointerPermissions = function(schema, className, operation, query, aclGroup = []) { + let perms = schema.perms[className]; + let field = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields'; + let userACL = aclGroup.filter((acl) => { + return acl.indexOf('role:') != 0 && acl != '*'; + }); + // the ACL should have exactly 1 user + if (perms && perms[field] && perms[field].length > 0) { + // No user set return undefined + if (userACL.length != 1) { + return; + } + let userId = userACL[0]; + let userPointer = { + "__type": "Pointer", + "className": "_User", + "objectId": userId + }; + + let constraints = {}; + let permFields = perms[field]; + let ors = permFields.map((key) => { + let q = { + [key]: userPointer + }; + return {'$and': [q, query]}; + }); + if (ors.length > 1) { + return {'$or': ors}; + } + return ors[0]; + } else { + return query; + } +} + function joinTableName(className, key) { return `_Join:${key}:${className}`; } diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index e2f06bc0..cf6b898a 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -112,8 +112,8 @@ function verifyPermissionKey(key) { } } -const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField']); -function validateCLP(perms) { +const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']); +function validateCLP(perms, fields) { if (!perms) { return; } @@ -121,6 +121,20 @@ function validateCLP(perms) { if (CLPValidKeys.indexOf(operation) == -1) { throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`); } + + if (operation === 'readUserFields' || operation === 'writeUserFields') { + if (!Array.isArray(perms[operation])) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${operation}`); + } else { + perms[operation].forEach((key) => { + if (!fields[key] || fields[key].type != 'Pointer' || fields[key].targetClass != '_User') { + throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid column for class level pointer permissions ${operation}`); + } + }); + } + return; + } + Object.keys(perms[operation]).forEach((key) => { verifyPermissionKey(key); let perm = perms[operation][key]; @@ -318,7 +332,7 @@ class SchemaController { }); return Promise.all(promises); }) - .then(() => this.setPermissions(className, classLevelPermissions)) + .then(() => this.setPermissions(className, classLevelPermissions, newSchema)) //TODO: Move this logic into the database adapter .then(() => ({ className: className, @@ -411,15 +425,15 @@ class SchemaController { error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.', }; } - validateCLP(classLevelPermissions); + validateCLP(classLevelPermissions, fields); } // Sets the Class-level permissions for a given className, which must exist. - setPermissions(className, perms) { + setPermissions(className, perms, newSchema) { if (typeof perms === 'undefined') { return Promise.resolve(); } - validateCLP(perms); + validateCLP(perms, newSchema); let update = { _metadata: { class_permissions: perms @@ -605,7 +619,8 @@ class SchemaController { if (!this.perms[className] || !this.perms[className][operation]) { return Promise.resolve(); } - let perms = this.perms[className][operation]; + let classPerms = this.perms[className]; + let perms = classPerms[operation]; // Handle the public scenario quickly if (perms['*']) { return Promise.resolve(); @@ -617,11 +632,26 @@ class SchemaController { found = true; } } - if (!found) { - // TODO: Verify correct error code - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, + + if (found) { + return Promise.resolve(); + } + + // No matching CLP, let's check the Pointer permissions + // And handle those later + let permissionField = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields'; + + // Reject create when write lockdown + if (permissionField == 'writeUserFields' && operation == 'create') { + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied for this action.'); } + + if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) { + return Promise.resolve(); + } + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, + 'Permission denied for this action.'); }; // Returns the expected type for a className+key combination