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
This commit is contained in:
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user