From d4fd73100c9895948f1192dd8cd6f070aa10eeff Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 14:49:09 -0500 Subject: [PATCH 1/9] Adds CLP API to Schema router --- spec/schemas.spec.js | 66 ++++++++++++++++++++++++++++++++++++ src/Routers/SchemasRouter.js | 20 +++++++++++ src/Schema.js | 9 +++++ 3 files changed, 95 insertions(+) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 9a410eed..312bd070 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -872,4 +872,70 @@ describe('schemas', () => { }); }); }); + + it('should set/get schema permissions', done => { + + let object = new Parse.Object('AClass'); + object.save().then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/AClass/permissions', + headers: masterKeyHeaders, + json: true, + body: { + find: { + '*': true + }, + create: { + 'role:admin': true + } + } + }, (error, response, body) => { + expect(error).toEqual(null); + request.get({ + url: 'http://localhost:8378/1/schemas/AClass/permissions', + headers: masterKeyHeaders, + json: true, + }, (error, response, body) => { + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({ + find: { + '*': true + }, + create: { + 'role:admin': true + } + }); + done(); + }); + }); + }); + }); + + it('should fail setting schema permissions with invalid key', done => { + + let object = new Parse.Object('AClass'); + object.save().then(() => { + request.put({ + url: 'http://localhost:8378/1/schemas/AClass/permissions', + headers: masterKeyHeaders, + json: true, + body: { + find: { + '*': true + }, + create: { + 'role:admin': true + }, + dummy: { + 'some': true + } + } + }, (error, response, body) => { + expect(error).toEqual(null); + expect(body.code).toEqual(107); + expect(body.error).toEqual('dummy is not a valid operation for class level permissions'); + done(); + }); + }); + }); }); diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index a0a90ef2..4f8acbe6 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -106,6 +106,24 @@ function modifySchema(req) { }); } +function setSchemaPermissions(req) { + var className = req.params.className; + return req.config.database.loadSchema() + .then(schema => { + return schema.setPermissions(className, req.body); + }).then((res) => { + return Promise.resolve({response: {}}); + }); +} + +function getSchemaPermissions(req) { + var className = req.params.className; + return req.config.database.loadSchema() + .then(schema => { + return Promise.resolve({response: schema.perms[className]}); + }); +} + // A helper function that removes all join tables for a schema. Returns a promise. var removeJoinTables = (database, mongoSchema) => { return Promise.all(Object.keys(mongoSchema) @@ -171,6 +189,8 @@ export class SchemasRouter extends PromiseRouter { this.route('POST', '/schemas', middleware.promiseEnforceMasterKeyAccess, createSchema); this.route('POST', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, createSchema); this.route('PUT', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, modifySchema); + this.route('GET', '/schemas/:className/permissions', middleware.promiseEnforceMasterKeyAccess, getSchemaPermissions); + this.route('PUT', '/schemas/:className/permissions', middleware.promiseEnforceMasterKeyAccess, setSchemaPermissions); this.route('DELETE', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, deleteSchema); } } diff --git a/src/Schema.js b/src/Schema.js index 2a048a54..496c0b2e 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -76,6 +76,14 @@ var requiredColumns = { _Role: ["name", "ACL"] } +let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete']; +function validateCLP(perms) { + Object.keys(perms).forEach((key) => { + if (CLPValidKeys.indexOf(key) == -1) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `${key} is not a valid operation for class level permissions`); + } + }); +} // Valid classes must: // Be one of _User, _Installation, _Role, _Session OR // Be a join table OR @@ -288,6 +296,7 @@ class Schema { // Sets the Class-level permissions for a given className, which must exist. setPermissions(className, perms) { + validateCLP(perms); var update = { _metadata: { class_permissions: perms From 5780c1e4250e34b367b4fe2c8317ed348183b242 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 21:38:20 -0500 Subject: [PATCH 2/9] Merges CLP endpoints with POST, PUT and GET --- spec/schemas.spec.js | 66 +++++++++++++++++----------------- src/Routers/SchemasRouter.js | 60 +++---------------------------- src/Schema.js | 69 ++++++++++++++++++++++++++++++++++-- 3 files changed, 104 insertions(+), 91 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 312bd070..bb40dbc2 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -874,14 +874,12 @@ describe('schemas', () => { }); it('should set/get schema permissions', done => { - - let object = new Parse.Object('AClass'); - object.save().then(() => { - request.put({ - url: 'http://localhost:8378/1/schemas/AClass/permissions', - headers: masterKeyHeaders, - json: true, - body: { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { find: { '*': true }, @@ -889,24 +887,24 @@ describe('schemas', () => { 'role:admin': true } } + } + }, (error, response, body) => { + expect(error).toEqual(null); + request.get({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, }, (error, response, body) => { - expect(error).toEqual(null); - request.get({ - url: 'http://localhost:8378/1/schemas/AClass/permissions', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - expect(response.body).toEqual({ - find: { - '*': true - }, - create: { - 'role:admin': true - } - }); - done(); + expect(response.statusCode).toEqual(200); + expect(response.body.classLevelPermissions).toEqual({ + find: { + '*': true + }, + create: { + 'role:admin': true + } }); + done(); }); }); }); @@ -916,18 +914,20 @@ describe('schemas', () => { let object = new Parse.Object('AClass'); object.save().then(() => { request.put({ - url: 'http://localhost:8378/1/schemas/AClass/permissions', + url: 'http://localhost:8378/1/schemas/AClass', headers: masterKeyHeaders, json: true, body: { - find: { - '*': true - }, - create: { - 'role:admin': true - }, - dummy: { - 'some': true + classLevelPermissions: { + find: { + '*': true + }, + create: { + 'role:admin': true + }, + dummy: { + 'some': true + } } } }, (error, response, body) => { diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index 4f8acbe6..49e4bbb2 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -46,7 +46,7 @@ function createSchema(req) { } return req.config.database.loadSchema() - .then(schema => schema.addClassIfNotExists(className, req.body.fields)) + .then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions)) .then(result => ({ response: Schema.mongoSchemaToSchemaAPIResponse(result) })); } @@ -60,62 +60,12 @@ function modifySchema(req) { return req.config.database.loadSchema() .then(schema => { - if (!schema.data[className]) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${req.params.className} does not exist.`); - } - - let existingFields = Object.assign(schema.data[className], { _id: className }); - Object.keys(submittedFields).forEach(name => { - let field = submittedFields[name]; - if (existingFields[name] && field.__op !== 'Delete') { - throw new Parse.Error(255, `Field ${name} exists, cannot update.`); - } - if (!existingFields[name] && field.__op === 'Delete') { - throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`); - } - }); - - let newSchema = Schema.buildMergedSchemaObject(existingFields, submittedFields); - let mongoObject = Schema.mongoSchemaFromFieldsAndClassName(newSchema, className); - if (!mongoObject.result) { - throw new Parse.Error(mongoObject.code, mongoObject.error); - } - - // Finally we have checked to make sure the request is valid and we can start deleting fields. - // Do all deletions first, then add fields to avoid duplicate geopoint error. - let deletePromises = []; - let insertedFields = []; - Object.keys(submittedFields).forEach(fieldName => { - if (submittedFields[fieldName].__op === 'Delete') { - const promise = schema.deleteField(fieldName, className, req.config.database); - deletePromises.push(promise); - } else { - insertedFields.push(fieldName); - } - }); - return Promise.all(deletePromises) // Delete Everything - .then(() => schema.reloadData()) // Reload our Schema, so we have all the new values - .then(() => { - let promises = insertedFields.map(fieldName => { - const mongoType = mongoObject.result[fieldName]; - return schema.validateField(className, fieldName, mongoType); - }); - return Promise.all(promises); - }) - .then(() => ({ response: Schema.mongoSchemaToSchemaAPIResponse(mongoObject.result) })); + return schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database); + }).then((result) => { + return Promise.resolve({response: result}); }); } -function setSchemaPermissions(req) { - var className = req.params.className; - return req.config.database.loadSchema() - .then(schema => { - return schema.setPermissions(className, req.body); - }).then((res) => { - return Promise.resolve({response: {}}); - }); -} - function getSchemaPermissions(req) { var className = req.params.className; return req.config.database.loadSchema() @@ -189,8 +139,6 @@ export class SchemasRouter extends PromiseRouter { this.route('POST', '/schemas', middleware.promiseEnforceMasterKeyAccess, createSchema); this.route('POST', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, createSchema); this.route('PUT', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, modifySchema); - this.route('GET', '/schemas/:className/permissions', middleware.promiseEnforceMasterKeyAccess, getSchemaPermissions); - this.route('PUT', '/schemas/:className/permissions', middleware.promiseEnforceMasterKeyAccess, setSchemaPermissions); this.route('DELETE', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, deleteSchema); } } diff --git a/src/Schema.js b/src/Schema.js index 496c0b2e..f1090c9c 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -78,6 +78,9 @@ var requiredColumns = { let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete']; function validateCLP(perms) { + if (!perms) { + return; + } Object.keys(perms).forEach((key) => { if (CLPValidKeys.indexOf(key) == -1) { throw new Parse.Error(Parse.Error.INVALID_JSON, `${key} is not a valid operation for class level permissions`); @@ -229,15 +232,23 @@ class Schema { // on success, and rejects with an error on fail. Ensure you // have authorization (master key, or client class creation // enabled) before calling this function. - addClassIfNotExists(className, fields) { + addClassIfNotExists(className, fields, classLevelPermissions) { 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); 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]) @@ -248,6 +259,56 @@ class Schema { return Promise.reject(error); }); } + + updateClass(className, submittedFields, classLevelPermissions, database) { + if (!this.data[className]) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); + } + let existingFields = Object.assign(this.data[className], {_id: className}); + Object.keys(submittedFields).forEach(name => { + let field = submittedFields[name]; + if (existingFields[name] && field.__op !== 'Delete') { + throw new Parse.Error(255, `Field ${name} exists, cannot update.`); + } + if (!existingFields[name] && field.__op === 'Delete') { + throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`); + } + }); + + validateCLP(classLevelPermissions); + let newSchema = buildMergedSchemaObject(existingFields, submittedFields); + let mongoObject = mongoSchemaFromFieldsAndClassName(newSchema, className); + 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 = []; + let insertedFields = []; + Object.keys(submittedFields).forEach(fieldName => { + if (submittedFields[fieldName].__op === 'Delete') { + const promise = this.deleteField(fieldName, className, database); + deletePromises.push(promise); + } else { + insertedFields.push(fieldName); + } + }); + return Promise.all(deletePromises) // Delete Everything + .then(() => this.reloadData()) // Reload our Schema, so we have all the new values + .then(() => { + let promises = insertedFields.map(fieldName => { + const mongoType = mongoObject.result[fieldName]; + return this.validateField(className, fieldName, mongoType); + }); + return Promise.all(promises); + }) + .then(() => { return mongoSchemaToSchemaAPIResponse(mongoObject.result) }); + } // Returns whether the schema knows the type of all these keys. @@ -785,10 +846,14 @@ function mongoSchemaAPIResponseFields(schema) { } function mongoSchemaToSchemaAPIResponse(schema) { - return { + let result = { className: schema._id, fields: mongoSchemaAPIResponseFields(schema), }; + if (schema._metadata && schema._metadata.class_permissions) { + result.classLevelPermissions = schema._metadata.class_permissions; + } + return result; } module.exports = { From 64f9fad285e52724e8953bbd37ea7e6a6ef44f60 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 22:27:11 -0500 Subject: [PATCH 3/9] Adds addField in CLP valid keys --- src/Schema.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Schema.js b/src/Schema.js index f1090c9c..c54b9008 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -76,7 +76,7 @@ var requiredColumns = { _Role: ["name", "ACL"] } -let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete']; +let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete', 'addField']; function validateCLP(perms) { if (!perms) { return; From e75d233b7eaeebd5ab29a114791ec30389c81747 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Mon, 7 Mar 2016 23:07:24 -0500 Subject: [PATCH 4/9] Adds validation of addFields --- spec/schemas.spec.js | 58 +++++++++++++++++++++++++++ src/Controllers/DatabaseController.js | 24 ++++++++++- src/RestWrite.js | 2 +- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index bb40dbc2..f37cf1ca 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -938,4 +938,62 @@ describe('schemas', () => { }); }); }); + + it('should not be able to add a field', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '*': true + }, + addField: { + 'role:admin': true + } + } + } + }, (error, response, body) => { + expect(error).toEqual(null); + let object = new Parse.Object('AClass'); + object.set('hello', 'world'); + return object.save().then(() => { + fail('should not be able to add a field'); + done(); + }, (err) => { + expect(err.message).toEqual('Permission denied for this action.'); + done(); + }) + }) + }); + + it('should not be able to add a field', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '*': true + }, + addField: { + '*': true + } + } + } + }, (error, response, body) => { + expect(error).toEqual(null); + let object = new Parse.Object('AClass'); + object.set('hello', 'world'); + return object.save().then(() => { + done(); + }, (err) => { + fail('should be able to add a field'); + done(); + }) + }) + }); + }); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index d5752088..3e85eca0 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -101,8 +101,12 @@ DatabaseController.prototype.redirectClassNameForKey = function(className, key) // Returns a promise that resolves to the new schema. // This does not update this.schema, because in a situation like a // batch request, that could confuse other users of the schema. -DatabaseController.prototype.validateObject = function(className, object, query) { - return this.loadSchema().then((schema) => { +DatabaseController.prototype.validateObject = function(className, object, query, options) { + let schema; + return this.loadSchema().then(s => { + schema = s; + return this.canAddField(schema, className, object, options.acl || []); + }).then(() => { return schema.validateObject(className, object, query); }); }; @@ -332,6 +336,22 @@ DatabaseController.prototype.create = function(className, object, options) { }); }; +DatabaseController.prototype.canAddField = function(schema, className, object, aclGroup) { + let classSchema = schema.data[className]; + if (!classSchema) { + return Promise.resolve(); + } + let fields = Object.keys(object); + let schemaFields = Object.keys(classSchema); + let newKeys = fields.filter((field) => { + return schemaFields.indexOf(field) < 0; + }) + if (newKeys.length > 0) { + return schema.validatePermission(className, aclGroup, 'addField'); + } + return Promise.resolve(); +} + // Runs a mongo query on the database. // This should only be used for testing - use 'find' for normal code // to avoid Mongo-format dependencies. diff --git a/src/RestWrite.js b/src/RestWrite.js index 9e07c93a..b68074d4 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -128,7 +128,7 @@ RestWrite.prototype.validateClientClassCreation = function() { // Validates this operation against the schema. RestWrite.prototype.validateSchema = function() { - return this.config.database.validateObject(this.className, this.data, this.query); + return this.config.database.validateObject(this.className, this.data, this.query, this.runOptions); }; // Runs any beforeSave triggers against this operation. From ddd1ae3338e1158261e9e4618fce84f7da5d5934 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 9 Mar 2016 19:58:50 -0500 Subject: [PATCH 5/9] Validates key, values and operation in CLP --- spec/schemas.spec.js | 126 +++++++++++++++++++++++++++++++++++++++++++ src/Schema.js | 32 +++++++++-- 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index f37cf1ca..2778be21 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -996,4 +996,130 @@ describe('schemas', () => { }) }); + it('should throw with invalid userId (>10 chars)', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '1234567890A': true + }, + } + } + }, (error, response, body) => { + expect(body.error).toEqual("'1234567890A' is not a valid key for class level permissions"); + done(); + }) + }); + + it('should throw with invalid userId (<10 chars)', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + 'a12345678': true + }, + } + } + }, (error, response, body) => { + expect(body.error).toEqual("'a12345678' is not a valid key for class level permissions"); + done(); + }) + }); + + it('should throw with invalid userId (invalid char)', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '12345_6789': true + }, + } + } + }, (error, response, body) => { + expect(body.error).toEqual("'12345_6789' is not a valid key for class level permissions"); + done(); + }) + }); + + it('should throw with invalid * (spaces)', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + ' *': true + }, + } + } + }, (error, response, body) => { + expect(body.error).toEqual("' *' is not a valid key for class level permissions"); + done(); + }) + }); + + it('should throw with invalid * (spaces)', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '* ': true + }, + } + } + }, (error, response, body) => { + expect(body.error).toEqual("'* ' is not a valid key for class level permissions"); + done(); + }) + }); + + it('should throw with invalid value', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '*': 1 + }, + } + } + }, (error, response, body) => { + expect(body.error).toEqual("'1' is not a valid value for class level permissions find:*:1"); + done(); + }) + }); + + it('should throw with invalid value', done => { + request.post({ + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '*': "" + }, + } + } + }, (error, response, body) => { + expect(body.error).toEqual("'' is not a valid value for class level permissions find:*:"); + done(); + }) + }); + }); diff --git a/src/Schema.js b/src/Schema.js index c54b9008..846a7490 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -76,15 +76,41 @@ var requiredColumns = { _Role: ["name", "ACL"] } +// 10 alpha numberic chars + uppercase +const userIdRegex = /^[a-zA-Z0-9]{10}$/; +// Anything that start with role +const roleRegex = /^role:.*/; +// * permission +const publicRegex = /^\*$/ + +const permissionKeyRegex = [userIdRegex, roleRegex, publicRegex]; + +function verifyPermissionKey(key) { + let result = permissionKeyRegex.reduce((isGood, regEx) => { + isGood = isGood || key.match(regEx) != null; + return isGood; + }, false); + if (!result) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid key for class level permissions`); + } +} + let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete', 'addField']; function validateCLP(perms) { if (!perms) { return; } - Object.keys(perms).forEach((key) => { - if (CLPValidKeys.indexOf(key) == -1) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `${key} is not a valid operation for class level permissions`); + Object.keys(perms).forEach((operation) => { + if (CLPValidKeys.indexOf(operation) == -1) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`); } + Object.keys(perms[operation]).forEach((key) => { + verifyPermissionKey(key); + let perm = perms[operation][key]; + if (perm !== true && perm !== false) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`); + } + }); }); } // Valid classes must: From d71a58c217af3902e177cf7044084ba825735506 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Wed, 9 Mar 2016 21:40:11 -0500 Subject: [PATCH 6/9] Adds tests, improve coverage, adds ability to delete CLP with classLevelPermissions: null --- spec/schemas.spec.js | 278 +++++++++++++++++++++++++++++++++++++++++++ src/Schema.js | 38 +++--- 2 files changed, 298 insertions(+), 18 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 2778be21..b249df8a 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 846a7490..b48bbfd9 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.setPermissions(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, From b1d399bf8013c42dec0043d70963b874fd4ec0be Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 10 Mar 2016 18:02:29 -0500 Subject: [PATCH 7/9] Adds blacklist permission, more test scenarios --- spec/schemas.spec.js | 161 ++++++++++++++++++++++++++++++++++++++----- src/Schema.js | 25 ++++--- 2 files changed, 159 insertions(+), 27 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index b249df8a..1350c74d 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1176,9 +1176,9 @@ describe('schemas', () => { }).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) => { + expect(err.message).toEqual('Permission denied for this action.'); return Promise.resolve(); }) }).then(() => { @@ -1193,7 +1193,6 @@ describe('schemas', () => { fail("should not fail!"); done(); }).catch( (err) => { - console.error(err); done(); }) }); @@ -1226,9 +1225,9 @@ describe('schemas', () => { }).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) => { + expect(err.message).toEqual('Permission denied for this action.'); return Promise.resolve(); }) }).then(() => { @@ -1244,8 +1243,6 @@ describe('schemas', () => { 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(); }); @@ -1258,11 +1255,9 @@ describe('schemas', () => { expect(results.length).toBe(1); done(); }, (err) => { - console.error(err); fail("should not fail!"); done(); }).catch( (err) => { - console.error(err); done(); }) }); @@ -1295,9 +1290,9 @@ describe('schemas', () => { }).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) => { + expect(err.message).toEqual('Permission denied for this action.'); return Promise.resolve(); }) }).then(() => { @@ -1308,7 +1303,6 @@ describe('schemas', () => { return query.find().then((result) => { expect(result.length).toBe(1); }, (err) => { - console.error(err); fail('User should be able to find!') done(); }); @@ -1321,13 +1315,9 @@ describe('schemas', () => { 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 => { @@ -1358,9 +1348,9 @@ describe('schemas', () => { }).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) => { + expect(err.message).toEqual('Permission denied for this action.'); return Promise.resolve(); }) }).then(() => { @@ -1391,13 +1381,150 @@ describe('schemas', () => { expect(results.length).toBe(1); done(); }, (err) => { - console.error(err); fail("should not fail!"); done(); }).catch( (err) => { - console.error(err); done(); }) }); + it('validate CLP 5', done => { + let user = new Parse.User(); + user.setUsername('user'); + user.setPassword('user'); + + let user2 = new Parse.User(); + user2.setUsername('user2'); + user2.setPassword('user2'); + let admin = new Parse.User(); + admin.setUsername('admin'); + admin.setPassword('admin'); + + let role = new Parse.Role('admin', new Parse.ACL()); + + Promise.resolve().then(() => { + return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true}); + }).then(()=> { + role.relation('users').add(admin); + return role.save(null, {useMasterKey: true}).then(() => { + let perm = { + 'find': { + // Admins can't read + 'role:admin': false + } + }; + // let the user find + perm['find'][user.id] = true; + return setPermissionsOnClass('AClass', perm); + }) + }).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((res) => { + expect(res.length).toEqual(1); + }, (err) => { + fail('User should be able to find!') + return Promise.resolve(); + }) + }).then(() => { + return Parse.User.logIn('admin', 'admin'); + }).then( () => { + let query = new Parse.Query('AClass'); + return query.find(); + }).then((results) => { + fail("should not be able to read!"); + return Promise.resolve(); + }, (err) => { + expect(err.message).toEqual('Permission denied for this action.'); + return Promise.resolve(); + }).then(() => { + return Parse.User.logIn('user2', 'user2'); + }).then( () => { + let query = new Parse.Query('AClass'); + return query.find(); + }).then((results) => { + fail("should not be able to read!"); + return Promise.resolve(); + }, (err) => { + expect(err.message).toEqual('Permission denied for this action.'); + return Promise.resolve(); + }).then(() => { + done(); + }); + }); + + it('validate CLP 6', done => { + let user = new Parse.User(); + user.setUsername('user'); + user.setPassword('user'); + + let user2 = new Parse.User(); + user2.setUsername('user2'); + user2.setPassword('user2'); + let admin = new Parse.User(); + admin.setUsername('admin'); + admin.setPassword('admin'); + + let role = new Parse.Role('admin', new Parse.ACL()); + + Promise.resolve().then(() => { + return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true}); + }).then(()=> { + role.relation('users').add(admin); + return role.save(null, {useMasterKey: true}).then(() => { + let perm = { + 'find': { + // Anyone can find + '*': true + } + }; + // but the user can't + perm['find'][user.id] = false; + return setPermissionsOnClass('AClass', perm); + }) + }).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((res) => { + fail('User should not be able to find!') + return Promise.resolve(); + }, (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).toEqual(1); + return Promise.resolve(); + }, (err) => { + fail('Should find the object as admin'); + return Promise.resolve(); + }).then(() => { + return Parse.User.logIn('user2', 'user2'); + }).then( () => { + let query = new Parse.Query('AClass'); + return query.find(); + }).then((results) => { + expect(results.length).toEqual(1); + return Promise.resolve(); + }, (err) => { + fail('Should find the object as user2'); + return Promise.resolve(); + }).then(() => { + done(); + }); + }); + }); diff --git a/src/Schema.js b/src/Schema.js index b48bbfd9..9b18517a 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -585,17 +585,22 @@ class Schema { return Promise.resolve(); } var perms = this.perms[className][operation]; - // Handle the public scenario quickly - if (perms['*']) { - return Promise.resolve(); - } - // Check permissions against the aclGroup provided (array of userId/roles) - var found = false; - for (var i = 0; i < aclGroup.length && !found; i++) { - if (perms[aclGroup[i]]) { - found = true; + + // Check permissions against the aclGroup provided (array of userId/roles) + // if perms has a public, check the blacklist + let startfound = perms['*'] ? true : undefined; + let found = aclGroup.reduce((memo, acl) => { + let perm = perms[acl]; + // We have a black listed permission + if (perm === false) { + return false; } - } + // the memo is not blacklisted + if (perm === true && memo !== false) { + return true; + } + return memo; + }, startfound); if (!found) { // TODO: Verify correct error code throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, From 16e3529c96959e72033f6c1e07d72486988953c8 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 10 Mar 2016 19:20:05 -0500 Subject: [PATCH 8/9] Removes blacklisting, *-but test case --- spec/schemas.spec.js | 78 ++------------------------------------------ src/Schema.js | 27 +++++++-------- 2 files changed, 13 insertions(+), 92 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 1350c74d..40e6150b 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1408,10 +1408,7 @@ describe('schemas', () => { role.relation('users').add(admin); return role.save(null, {useMasterKey: true}).then(() => { let perm = { - 'find': { - // Admins can't read - 'role:admin': false - } + find: {} }; // let the user find perm['find'][user.id] = true; @@ -1455,76 +1452,5 @@ describe('schemas', () => { }).then(() => { done(); }); - }); - - it('validate CLP 6', done => { - let user = new Parse.User(); - user.setUsername('user'); - user.setPassword('user'); - - let user2 = new Parse.User(); - user2.setUsername('user2'); - user2.setPassword('user2'); - let admin = new Parse.User(); - admin.setUsername('admin'); - admin.setPassword('admin'); - - let role = new Parse.Role('admin', new Parse.ACL()); - - Promise.resolve().then(() => { - return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true}); - }).then(()=> { - role.relation('users').add(admin); - return role.save(null, {useMasterKey: true}).then(() => { - let perm = { - 'find': { - // Anyone can find - '*': true - } - }; - // but the user can't - perm['find'][user.id] = false; - return setPermissionsOnClass('AClass', perm); - }) - }).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((res) => { - fail('User should not be able to find!') - return Promise.resolve(); - }, (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).toEqual(1); - return Promise.resolve(); - }, (err) => { - fail('Should find the object as admin'); - return Promise.resolve(); - }).then(() => { - return Parse.User.logIn('user2', 'user2'); - }).then( () => { - let query = new Parse.Query('AClass'); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - return Promise.resolve(); - }, (err) => { - fail('Should find the object as user2'); - return Promise.resolve(); - }).then(() => { - done(); - }); - }); - + }); }); diff --git a/src/Schema.js b/src/Schema.js index 9b18517a..f4e1b9bf 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -107,7 +107,7 @@ function validateCLP(perms) { Object.keys(perms[operation]).forEach((key) => { verifyPermissionKey(key); let perm = perms[operation][key]; - if (perm !== true && perm !== false) { + if (perm !== true) { throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`); } }); @@ -585,22 +585,17 @@ class Schema { return Promise.resolve(); } var perms = this.perms[className][operation]; - - // Check permissions against the aclGroup provided (array of userId/roles) - // if perms has a public, check the blacklist - let startfound = perms['*'] ? true : undefined; - let found = aclGroup.reduce((memo, acl) => { - let perm = perms[acl]; - // We have a black listed permission - if (perm === false) { - return false; + // Handle the public scenario quickly + if (perms['*']) { + return Promise.resolve(); + } + // Check permissions against the aclGroup provided (array of userId/roles) + var found = false; + for (var i = 0; i < aclGroup.length && !found; i++) { + if (perms[aclGroup[i]]) { + found = true; } - // the memo is not blacklisted - if (perm === true && memo !== false) { - return true; - } - return memo; - }, startfound); + } if (!found) { // TODO: Verify correct error code throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, From c935ed836420f06f02958bdcb11e006a35e06d8a Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Thu, 10 Mar 2016 23:01:45 -0500 Subject: [PATCH 9/9] Always return default public permissions --- spec/schemas.spec.js | 59 ++++++++++++++++++++++++++++++++++++++------ src/Schema.js | 14 +++++++++-- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 40e6150b..e9195615 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -23,6 +23,27 @@ var hasAllPODobject = () => { return obj; }; +let defaultClassLevelPermissions = { + find: { + '*': true + }, + create: { + '*': true + }, + get: { + '*': true + }, + update: { + '*': true + }, + addField: { + '*': true + }, + delete: { + '*': true + } +} + var plainOldDataSchema = { className: 'HasAllPOD', fields: { @@ -40,7 +61,8 @@ var plainOldDataSchema = { aArray: {type: 'Array'}, aGeoPoint: {type: 'GeoPoint'}, aFile: {type: 'File'} - } + }, + classLevelPermissions: defaultClassLevelPermissions }; var pointersAndRelationsSchema = { @@ -61,6 +83,7 @@ var pointersAndRelationsSchema = { targetClass: 'HasAllPOD', }, }, + classLevelPermissions: defaultClassLevelPermissions } var noAuthHeaders = { @@ -296,7 +319,8 @@ describe('schemas', () => { objectId: {type: 'String'}, foo: {type: 'Number'}, ptr: {type: 'Pointer', targetClass: 'SomeClass'}, - } + }, + classLevelPermissions: defaultClassLevelPermissions }); done(); }); @@ -318,7 +342,8 @@ describe('schemas', () => { createdAt: {type: 'Date'}, updatedAt: {type: 'Date'}, objectId: {type: 'String'}, - } + }, + classLevelPermissions: defaultClassLevelPermissions }); done(); }); @@ -490,7 +515,8 @@ describe('schemas', () => { "objectId": {"type": "String"}, "updatedAt": {"type": "Date"}, "geo2": {"type": "GeoPoint"}, - } + }, + classLevelPermissions: defaultClassLevelPermissions })).toEqual(undefined); done(); }); @@ -539,6 +565,7 @@ describe('schemas', () => { "updatedAt": {"type": "Date"}, "newField": {"type": "String"}, }, + classLevelPermissions: defaultClassLevelPermissions })).toEqual(undefined); request.get({ url: 'http://localhost:8378/1/schemas/NewClass', @@ -553,7 +580,8 @@ describe('schemas', () => { updatedAt: {type: 'Date'}, objectId: {type: 'String'}, newField: {type: 'String'}, - } + }, + classLevelPermissions: defaultClassLevelPermissions }); done(); }); @@ -590,7 +618,8 @@ describe('schemas', () => { emailVerified: {type: 'Boolean'}, newField: {type: 'String'}, ACL: {type: 'ACL'} - } + }, + classLevelPermissions: defaultClassLevelPermissions }); request.get({ url: 'http://localhost:8378/1/schemas/_User', @@ -610,7 +639,8 @@ describe('schemas', () => { emailVerified: {type: 'Boolean'}, newField: {type: 'String'}, ACL: {type: 'ACL'} - } + }, + classLevelPermissions: defaultClassLevelPermissions }); done(); }); @@ -656,7 +686,8 @@ describe('schemas', () => { aNewString: {type: 'String'}, aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'}, aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'}, - } + }, + classLevelPermissions: defaultClassLevelPermissions }); var obj2 = new Parse.Object('HasAllPOD'); obj2.set('aNewPointer', obj1); @@ -902,6 +933,18 @@ describe('schemas', () => { }, create: { 'role:admin': true + }, + get: { + '*': true + }, + update: { + '*': true + }, + addField: { + '*': true + }, + delete: { + '*': true } }); done(); diff --git a/src/Schema.js b/src/Schema.js index f4e1b9bf..ffb7b088 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -96,6 +96,13 @@ function verifyPermissionKey(key) { } let CLPValidKeys = ['find', 'get', 'create', 'update', 'delete', 'addField']; +let DefaultClassLevelPermissions = CLPValidKeys.reduce((perms, key) => { + perms[key] = { + '*': true + }; + return perms; + }, {}); + function validateCLP(perms) { if (!perms) { return; @@ -879,9 +886,12 @@ function mongoSchemaToSchemaAPIResponse(schema) { className: schema._id, fields: mongoSchemaAPIResponseFields(schema), }; + + let classLevelPermissions = DefaultClassLevelPermissions; if (schema._metadata && schema._metadata.class_permissions) { - result.classLevelPermissions = schema._metadata.class_permissions; - } + classLevelPermissions = Object.assign(classLevelPermissions, schema._metadata.class_permissions); + } + result.classLevelPermissions = classLevelPermissions; return result; }