Merge pull request #898 from ParsePlatform/flovilmart.CLPAPI
Adds CLP API to Schema router
This commit is contained in:
@@ -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);
|
||||
@@ -872,4 +903,597 @@ describe('schemas', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set/get schema permissions', done => {
|
||||
request.post({
|
||||
url: 'http://localhost:8378/1/schemas/AClass',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {
|
||||
classLevelPermissions: {
|
||||
find: {
|
||||
'*': true
|
||||
},
|
||||
create: {
|
||||
'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(response.statusCode).toEqual(200);
|
||||
expect(response.body.classLevelPermissions).toEqual({
|
||||
find: {
|
||||
'*': true
|
||||
},
|
||||
create: {
|
||||
'role:admin': true
|
||||
},
|
||||
get: {
|
||||
'*': true
|
||||
},
|
||||
update: {
|
||||
'*': true
|
||||
},
|
||||
addField: {
|
||||
'*': true
|
||||
},
|
||||
delete: {
|
||||
'*': 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',
|
||||
headers: masterKeyHeaders,
|
||||
json: true,
|
||||
body: {
|
||||
classLevelPermissions: {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
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();
|
||||
})
|
||||
});
|
||||
|
||||
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) => {
|
||||
fail('Use should hot 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();
|
||||
}, () => {
|
||||
fail("should not fail!");
|
||||
done();
|
||||
}).catch( (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) => {
|
||||
fail('User should not be able to find!')
|
||||
}, (err) => {
|
||||
expect(err.message).toEqual('Permission denied for this action.');
|
||||
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) => {
|
||||
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) => {
|
||||
fail("should not fail!");
|
||||
done();
|
||||
}).catch( (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) => {
|
||||
fail('User should not be able to find!')
|
||||
}, (err) => {
|
||||
expect(err.message).toEqual('Permission denied for this action.');
|
||||
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) => {
|
||||
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) => {
|
||||
fail("should not fail!");
|
||||
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) => {
|
||||
fail('User should not be able to find!')
|
||||
}, (err) => {
|
||||
expect(err.message).toEqual('Permission denied for this action.');
|
||||
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) => {
|
||||
fail("should not fail!");
|
||||
done();
|
||||
}).catch( (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: {}
|
||||
};
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
122
src/Schema.js
122
src/Schema.js
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user