From b7b57f7ea73e075eb3731c352ae24b66eb95a90a Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 20 Apr 2016 21:51:11 -0400 Subject: [PATCH] Adds support for Pointer Permissions * WIP: Initial pointer permissions * Process Pointer perms when no matching CLP are found * Additional tests with read lockdown * Create operation lockdown with pointer permissions, on parse.com, when an class is locked down with write pointer perm, users can't create objects even if they set their own as the pointer permission key * Adds test case for multiple write PointerPerms * Adds validation for pointer permissions when setting * Adds tests for validating pointer permissions column types * Adds tests for complex ACL/CLP/PP hierarchy * Restores power of the master * Adds validation of borked fields * Adds complex test for find * Adds more variations around PointerPermissions tests --- spec/PointerPermissions.spec.js | 697 ++++++++++++++++++++++++++ src/Controllers/DatabaseController.js | 65 ++- src/Controllers/SchemaController.js | 50 +- 3 files changed, 799 insertions(+), 13 deletions(-) create mode 100644 spec/PointerPermissions.spec.js 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