feat: Add default ACL (#8701)
This commit is contained in:
@@ -18,6 +18,12 @@ describe('MongoSchemaCollection', () => {
|
|||||||
},
|
},
|
||||||
_metadata: {
|
_metadata: {
|
||||||
class_permissions: {
|
class_permissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -69,6 +75,12 @@ describe('MongoSchemaCollection', () => {
|
|||||||
objectId: { type: 'String' },
|
objectId: { type: 'String' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
|
|||||||
@@ -923,4 +923,32 @@ describe('Parse.ACL', () => {
|
|||||||
|
|
||||||
rest.create(config, auth.nobody(config), '_User', anonUser);
|
rest.create(config, auth.nobody(config), '_User', anonUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('support defaultACL in schema', async () => {
|
||||||
|
await new Parse.Object('TestObject').save();
|
||||||
|
const schema = await Parse.Server.database.loadSchema();
|
||||||
|
await schema.updateClass(
|
||||||
|
'TestObject',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
create: {
|
||||||
|
'*': true,
|
||||||
|
},
|
||||||
|
ACL: {
|
||||||
|
'*': { read: true },
|
||||||
|
currentUser: { read: true, write: true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const acls = new Parse.ACL();
|
||||||
|
acls.setPublicReadAccess(true);
|
||||||
|
const user = await Parse.User.signUp('testuser', 'p@ssword');
|
||||||
|
const obj = new Parse.Object('TestObject');
|
||||||
|
await obj.save(null, { sessionToken: user.getSessionToken() });
|
||||||
|
expect(obj.getACL()).toBeDefined();
|
||||||
|
const acl = obj.getACL().toJSON();
|
||||||
|
expect(acl['*']).toEqual({ read: true });
|
||||||
|
expect(acl[user.id].write).toBeTrue();
|
||||||
|
expect(acl[user.id].read).toBeTrue();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -309,6 +309,12 @@ describe('SchemaController', () => {
|
|||||||
foo: { type: 'String' },
|
foo: { type: 'String' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -329,6 +335,12 @@ describe('SchemaController', () => {
|
|||||||
|
|
||||||
it('can update classes without needing an object', done => {
|
it('can update classes without needing an object', done => {
|
||||||
const levelPermissions = {
|
const levelPermissions = {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -489,6 +501,12 @@ describe('SchemaController', () => {
|
|||||||
foo: { type: 'String' },
|
foo: { type: 'String' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -694,6 +712,12 @@ describe('SchemaController', () => {
|
|||||||
|
|
||||||
it('refuses to add CLP with incorrect find', done => {
|
it('refuses to add CLP with incorrect find', done => {
|
||||||
const levelPermissions = {
|
const levelPermissions = {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': false },
|
find: { '*': false },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
create: { '*': true },
|
create: { '*': true },
|
||||||
@@ -717,6 +741,12 @@ describe('SchemaController', () => {
|
|||||||
|
|
||||||
it('refuses to add CLP when incorrectly sending a string to protectedFields object value instead of an array', done => {
|
it('refuses to add CLP when incorrectly sending a string to protectedFields object value instead of an array', done => {
|
||||||
const levelPermissions = {
|
const levelPermissions = {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
create: { '*': true },
|
create: { '*': true },
|
||||||
@@ -785,6 +815,12 @@ describe('SchemaController', () => {
|
|||||||
aPolygon: { type: 'Polygon' },
|
aPolygon: { type: 'Polygon' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -832,6 +868,12 @@ describe('SchemaController', () => {
|
|||||||
parseVersion: { type: 'String' },
|
parseVersion: { type: 'String' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -866,6 +908,12 @@ describe('SchemaController', () => {
|
|||||||
roles: { type: 'Relation', targetClass: '_Role' },
|
roles: { type: 'Relation', targetClass: '_Role' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -900,6 +948,12 @@ describe('SchemaController', () => {
|
|||||||
ACL: { type: 'ACL' },
|
ACL: { type: 'ACL' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -1070,6 +1124,12 @@ describe('SchemaController', () => {
|
|||||||
relationField: { type: 'Relation', targetClass: '_User' },
|
relationField: { type: 'Relation', targetClass: '_User' },
|
||||||
},
|
},
|
||||||
classLevelPermissions: {
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ describe('Schema Performance', function () {
|
|||||||
await schema.reloadData();
|
await schema.reloadData();
|
||||||
|
|
||||||
const levelPermissions = {
|
const levelPermissions = {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
create: { '*': true },
|
create: { '*': true },
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ const hasAllPODobject = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultClassLevelPermissions = {
|
const defaultClassLevelPermissions = {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: {
|
find: {
|
||||||
'*': true,
|
'*': true,
|
||||||
},
|
},
|
||||||
@@ -2058,12 +2064,70 @@ describe('schemas', () => {
|
|||||||
},
|
},
|
||||||
}).then(fail, response => {
|
}).then(fail, response => {
|
||||||
expect(response.data.error).toEqual(
|
expect(response.data.error).toEqual(
|
||||||
"'1' is not a valid value for class level permissions find:*:1"
|
"'1' is not a valid value for class level permissions acl find:*"
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should validate defaultAcl with class level permissions when request is not an object', async () => {
|
||||||
|
const response = await request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/schemas/AClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).catch(error => error.data);
|
||||||
|
|
||||||
|
expect(response.error).toEqual(`'true' is not a valid value for class level permissions acl`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate defaultAcl with class level permissions when request is an object and invalid key', async () => {
|
||||||
|
const response = await request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/schemas/AClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
foo: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).catch(error => error.data);
|
||||||
|
|
||||||
|
expect(response.error).toEqual(`'foo' is not a valid key for class level permissions acl`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate defaultAcl with class level permissions when request is an object and invalid value', async () => {
|
||||||
|
const response = await request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'http://localhost:8378/1/schemas/AClass',
|
||||||
|
headers: masterKeyHeaders,
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
classLevelPermissions: {
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).catch(error => error.data);
|
||||||
|
|
||||||
|
expect(response.error).toEqual(`'1' is not a valid value for class level permissions acl`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw if permission is empty string', done => {
|
it('should throw if permission is empty string', done => {
|
||||||
request({
|
request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -2079,7 +2143,7 @@ describe('schemas', () => {
|
|||||||
},
|
},
|
||||||
}).then(fail, response => {
|
}).then(fail, response => {
|
||||||
expect(response.data.error).toEqual(
|
expect(response.data.error).toEqual(
|
||||||
"'' is not a valid value for class level permissions find:*:"
|
`'' is not a valid value for class level permissions acl find:*`
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -2690,6 +2754,12 @@ describe('schemas', () => {
|
|||||||
setPermissionsOnClass(
|
setPermissionsOnClass(
|
||||||
'_Role',
|
'_Role',
|
||||||
{
|
{
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
@@ -2710,6 +2780,12 @@ describe('schemas', () => {
|
|||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
expect(res.data.classLevelPermissions).toEqual({
|
expect(res.data.classLevelPermissions).toEqual({
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ const emptyCLPS = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultCLPS = Object.freeze({
|
const defaultCLPS = Object.freeze({
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
|
|||||||
@@ -127,6 +127,12 @@ const emptyCLPS = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultCLPS = Object.freeze({
|
const defaultCLPS = Object.freeze({
|
||||||
|
ACL: {
|
||||||
|
'*': {
|
||||||
|
read: true,
|
||||||
|
write: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
find: { '*': true },
|
find: { '*': true },
|
||||||
get: { '*': true },
|
get: { '*': true },
|
||||||
count: { '*': true },
|
count: { '*': true },
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ function validateProtectedFieldsKey(key, userIdRegExp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CLPValidKeys = Object.freeze([
|
const CLPValidKeys = Object.freeze([
|
||||||
|
'ACL',
|
||||||
'find',
|
'find',
|
||||||
'count',
|
'count',
|
||||||
'get',
|
'get',
|
||||||
@@ -364,13 +365,34 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields, userIdR
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// or [entity]: boolean
|
|
||||||
const permit = operation[entity];
|
const permit = operation[entity];
|
||||||
|
|
||||||
if (permit !== true) {
|
if (operationKey === 'ACL') {
|
||||||
|
if (Object.prototype.toString.call(permit) !== '[object Object]') {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
`'${permit}' is not a valid value for class level permissions acl`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const invalidKeys = Object.keys(permit).filter(key => !['read', 'write'].includes(key));
|
||||||
|
const invalidValues = Object.values(permit).filter(key => typeof key !== 'boolean');
|
||||||
|
if (invalidKeys.length) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
`'${invalidKeys.join(',')}' is not a valid key for class level permissions acl`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidValues.length) {
|
||||||
|
throw new Parse.Error(
|
||||||
|
Parse.Error.INVALID_JSON,
|
||||||
|
`'${invalidValues.join(',')}' is not a valid value for class level permissions acl`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (permit !== true) {
|
||||||
throw new Parse.Error(
|
throw new Parse.Error(
|
||||||
Parse.Error.INVALID_JSON,
|
Parse.Error.INVALID_JSON,
|
||||||
`'${permit}' is not a valid value for class level permissions ${operationKey}:${entity}:${permit}`
|
`'${permit}' is not a valid value for class level permissions acl ${operationKey}:${entity}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ export type Schema = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ClassLevelPermissions = {
|
export type ClassLevelPermissions = {
|
||||||
|
ACL?: {
|
||||||
|
[string]: {
|
||||||
|
[string]: boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
find?: { [string]: boolean },
|
find?: { [string]: boolean },
|
||||||
count?: { [string]: boolean },
|
count?: { [string]: boolean },
|
||||||
get?: { [string]: boolean },
|
get?: { [string]: boolean },
|
||||||
|
|||||||
@@ -367,6 +367,25 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// add default ACL
|
||||||
|
if (
|
||||||
|
schema?.classLevelPermissions?.ACL &&
|
||||||
|
!this.data.ACL &&
|
||||||
|
JSON.stringify(schema.classLevelPermissions.ACL) !==
|
||||||
|
JSON.stringify({ '*': { read: true, write: true } })
|
||||||
|
) {
|
||||||
|
const acl = deepcopy(schema.classLevelPermissions.ACL);
|
||||||
|
if (acl.currentUser) {
|
||||||
|
if (this.auth.user?.id) {
|
||||||
|
acl[this.auth.user?.id] = deepcopy(acl.currentUser);
|
||||||
|
}
|
||||||
|
delete acl.currentUser;
|
||||||
|
}
|
||||||
|
this.data.ACL = acl;
|
||||||
|
this.storage.fieldsChangedByTrigger = this.storage.fieldsChangedByTrigger || [];
|
||||||
|
this.storage.fieldsChangedByTrigger.push('ACL');
|
||||||
|
}
|
||||||
|
|
||||||
// Add default fields
|
// Add default fields
|
||||||
if (!this.query) {
|
if (!this.query) {
|
||||||
// allow customizing createdAt and updatedAt when using maintenance key
|
// allow customizing createdAt and updatedAt when using maintenance key
|
||||||
|
|||||||
Reference in New Issue
Block a user