Merge pull request #898 from ParsePlatform/flovilmart.CLPAPI

Adds CLP API to Schema router
This commit is contained in:
Florent Vilmart
2016-03-11 00:32:41 -05:00
5 changed files with 784 additions and 60 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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,52 +60,20 @@ 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 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)

View File

@@ -76,6 +76,50 @@ 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'];
let DefaultClassLevelPermissions = CLPValidKeys.reduce((perms, key) => {
perms[key] = {
'*': true
};
return perms;
}, {});
function validateCLP(perms) {
if (!perms) {
return;
}
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) {
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`);
}
});
});
}
// Valid classes must:
// Be one of _User, _Installation, _Role, _Session OR
// Be a join table OR
@@ -221,12 +265,12 @@ 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.`);
}
let mongoObject = mongoSchemaFromFieldsAndClassName(fields, className);
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions);
if (!mongoObject.result) {
return Promise.reject(mongoObject);
}
@@ -240,6 +284,54 @@ 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.`);
}
});
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions);
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 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 this.setPermissions(className, classLevelPermissions)
})
.then(() => { return mongoSchemaToSchemaAPIResponse(mongoObject.result) });
}
// Returns whether the schema knows the type of all these keys.
@@ -288,6 +380,10 @@ 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: {
class_permissions: perms
@@ -548,7 +644,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,
@@ -601,6 +697,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 };
}
@@ -776,17 +882,23 @@ function mongoSchemaAPIResponseFields(schema) {
}
function mongoSchemaToSchemaAPIResponse(schema) {
return {
let result = {
className: schema._id,
fields: mongoSchemaAPIResponseFields(schema),
};
let classLevelPermissions = DefaultClassLevelPermissions;
if (schema._metadata && schema._metadata.class_permissions) {
classLevelPermissions = Object.assign(classLevelPermissions, schema._metadata.class_permissions);
}
result.classLevelPermissions = classLevelPermissions;
return result;
}
module.exports = {
load: load,
classNameIsValid: classNameIsValid,
invalidClassNameMessage: invalidClassNameMessage,
mongoSchemaFromFieldsAndClassName: mongoSchemaFromFieldsAndClassName,
schemaAPITypeToMongoFieldType: schemaAPITypeToMongoFieldType,
buildMergedSchemaObject: buildMergedSchemaObject,
mongoFieldTypeToSchemaAPIType: mongoFieldTypeToSchemaAPIType,