From 5d10732fec1f889a9eaabad998005ab527024032 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 17:36:25 +1000 Subject: [PATCH 01/10] feat: add default acl --- spec/ParseACL.spec.js | 28 ++++++++++++++++++++++++++++ src/Controllers/SchemaController.js | 19 ++++++++++++++++++- src/RestWrite.js | 14 ++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index a55f40cd42..57d1a4b750 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -931,4 +931,32 @@ describe('Parse.ACL', () => { 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(); + }); }); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ad3699aaa5..53fbd7c834 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -255,6 +255,7 @@ function validateProtectedFieldsKey(key, userIdRegExp) { } const CLPValidKeys = Object.freeze([ + 'ACL', 'find', 'count', 'get', @@ -367,7 +368,23 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields, userIdR // or [entity]: boolean 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 ${operationKey}:${entity}:${permit}` + ); + } + const isValid = + Object.keys(permit).some(key => !['read', 'write'].includes(key)) || + Object.values(permit).some(key => typeof key !== Boolean); + if (!isValid) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `'${permit}' is not a valid value for class level permissions ${operationKey}:${entity}:${permit}` + ); + } + } else if (permit !== true) { throw new Parse.Error( Parse.Error.INVALID_JSON, `'${permit}' is not a valid value for class level permissions ${operationKey}:${entity}:${permit}` diff --git a/src/RestWrite.js b/src/RestWrite.js index d35e52d6b5..d9ecb8d3cd 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -367,6 +367,20 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () { } }; + // add default ACL + if (schema.classLevelPermissions.ACL && !this.data.ACL) { + 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 this.data.updatedAt = this.updatedAt; if (!this.query) { From d531e39debd7abd31f140fba3c3c323f3299d260 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 17:39:21 +1000 Subject: [PATCH 02/10] add default acl --- src/Adapters/Storage/Mongo/MongoSchemaCollection.js | 6 ++++++ src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 6 ++++++ src/RestWrite.js | 7 ++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 20e3eec325..45b27f7516 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -76,6 +76,12 @@ const emptyCLPS = Object.freeze({ }); const defaultCLPS = Object.freeze({ + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, count: { '*': true }, get: { '*': true }, diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 3ad59ec77f..94a894d379 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -127,6 +127,12 @@ const emptyCLPS = Object.freeze({ }); const defaultCLPS = Object.freeze({ + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, diff --git a/src/RestWrite.js b/src/RestWrite.js index d9ecb8d3cd..5dae243163 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -368,7 +368,12 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () { }; // add default ACL - if (schema.classLevelPermissions.ACL && !this.data.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) { From 55273e40f7c318bb4048f0e6afef7d29f4545e5c Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 17:49:56 +1000 Subject: [PATCH 03/10] Update types.js --- src/Controllers/types.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Controllers/types.js b/src/Controllers/types.js index 2bd3298935..c509665959 100644 --- a/src/Controllers/types.js +++ b/src/Controllers/types.js @@ -19,6 +19,11 @@ export type Schema = { }; export type ClassLevelPermissions = { + ACL?: { + [string]: { + [string]: Boolean, + }, + }, find?: { [string]: boolean }, count?: { [string]: boolean }, get?: { [string]: boolean }, From 455842973e241e412de219115113a77681d22797 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 18:02:46 +1000 Subject: [PATCH 04/10] wip --- src/Controllers/SchemaController.js | 9 ++++++++- src/Controllers/types.js | 2 +- src/RestWrite.js | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 53fbd7c834..8ef878ce3e 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -365,10 +365,17 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields, userIdR continue; } - // or [entity]: boolean const permit = operation[entity]; if (operationKey === 'ACL') { + if (permit === true) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `'${ + permit ? 'true' : 'false' + }' is not a valid value for class level permissions ${operationKey}:${entity}` + ); + } if (Object.prototype.toString.call(permit) !== '[object Object]') { throw new Parse.Error( Parse.Error.INVALID_JSON, diff --git a/src/Controllers/types.js b/src/Controllers/types.js index c509665959..98e41fd2d3 100644 --- a/src/Controllers/types.js +++ b/src/Controllers/types.js @@ -21,7 +21,7 @@ export type Schema = { export type ClassLevelPermissions = { ACL?: { [string]: { - [string]: Boolean, + [string]: boolean, }, }, find?: { [string]: boolean }, diff --git a/src/RestWrite.js b/src/RestWrite.js index 5dae243163..83d416d313 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -369,7 +369,7 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function () { // add default ACL if ( - schema.classLevelPermissions.ACL && + schema?.classLevelPermissions?.ACL && !this.data.ACL && JSON.stringify(schema.classLevelPermissions.ACL) !== JSON.stringify({ '*': { read: true, write: true } }) From 17e0ad3c2dc24aa2fc88b1fa373c9402660438d9 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 22:36:51 +1000 Subject: [PATCH 05/10] wip --- spec/MongoSchemaCollectionAdapter.spec.js | 12 +++++ spec/Schema.spec.js | 60 +++++++++++++++++++++++ spec/SchemaPerformance.spec.js | 6 +++ spec/schemas.spec.js | 12 +++++ 4 files changed, 90 insertions(+) diff --git a/spec/MongoSchemaCollectionAdapter.spec.js b/spec/MongoSchemaCollectionAdapter.spec.js index 242466a18d..8e376b9d1d 100644 --- a/spec/MongoSchemaCollectionAdapter.spec.js +++ b/spec/MongoSchemaCollectionAdapter.spec.js @@ -18,6 +18,12 @@ describe('MongoSchemaCollection', () => { }, _metadata: { class_permissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, get: { '*': true }, find: { '*': true }, count: { '*': true }, @@ -69,6 +75,12 @@ describe('MongoSchemaCollection', () => { objectId: { type: 'String' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 7f5b5650fc..2192678797 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -309,6 +309,12 @@ describe('SchemaController', () => { foo: { type: 'String' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, @@ -329,6 +335,12 @@ describe('SchemaController', () => { it('can update classes without needing an object', done => { const levelPermissions = { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, @@ -489,6 +501,12 @@ describe('SchemaController', () => { foo: { type: 'String' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, @@ -694,6 +712,12 @@ describe('SchemaController', () => { it('refuses to add CLP with incorrect find', done => { const levelPermissions = { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': false }, get: { '*': 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 => { const levelPermissions = { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, create: { '*': true }, @@ -785,6 +815,12 @@ describe('SchemaController', () => { aPolygon: { type: 'Polygon' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, @@ -832,6 +868,12 @@ describe('SchemaController', () => { parseVersion: { type: 'String' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, @@ -866,6 +908,12 @@ describe('SchemaController', () => { roles: { type: 'Relation', targetClass: '_Role' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, @@ -900,6 +948,12 @@ describe('SchemaController', () => { ACL: { type: 'ACL' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, @@ -1070,6 +1124,12 @@ describe('SchemaController', () => { relationField: { type: 'Relation', targetClass: '_User' }, }, classLevelPermissions: { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, count: { '*': true }, diff --git a/spec/SchemaPerformance.spec.js b/spec/SchemaPerformance.spec.js index 17238b0ed6..75ce9718ed 100644 --- a/spec/SchemaPerformance.spec.js +++ b/spec/SchemaPerformance.spec.js @@ -167,6 +167,12 @@ describe('Schema Performance', function () { await schema.reloadData(); const levelPermissions = { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true }, get: { '*': true }, create: { '*': true }, diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 9557dd7924..35a772b1d5 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2690,6 +2690,12 @@ describe('schemas', () => { setPermissionsOnClass( '_Role', { + ACL: { + '*': { + read: true, + write: true, + }, + }, get: { '*': true }, find: { '*': true }, count: { '*': true }, @@ -2710,6 +2716,12 @@ describe('schemas', () => { }) .then(res => { expect(res.data.classLevelPermissions).toEqual({ + ACL: { + '*': { + read: true, + write: true, + }, + }, get: { '*': true }, find: { '*': true }, count: { '*': true }, From 68bc76a441aebd76ce7d81614295535f1e25cfc8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Fri, 28 Jul 2023 01:50:49 +1000 Subject: [PATCH 06/10] Update schemas.spec.js --- spec/schemas.spec.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 35a772b1d5..ba1ab06a74 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -26,6 +26,12 @@ const hasAllPODobject = () => { }; const defaultClassLevelPermissions = { + ACL: { + '*': { + read: true, + write: true, + }, + }, find: { '*': true, }, From bf8103c7527beebfc398d669cb7456bbea309d99 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 20 Mar 2025 20:13:40 +1100 Subject: [PATCH 07/10] add tests --- spec/schemas.spec.js | 60 ++++++++++++++++++++++++++++- src/Controllers/SchemaController.js | 20 +++++----- 2 files changed, 68 insertions(+), 12 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index ba3efdc158..cf0801418c 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2070,6 +2070,64 @@ describe('schemas', () => { }); }); + 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 => { request({ method: 'POST', @@ -2085,7 +2143,7 @@ describe('schemas', () => { }, }).then(fail, response => { expect(response.data.error).toEqual( - "'' is not a valid value for class level permissions find:*:" + `'1' is not a valid value for class level permissions acl` ); done(); }); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index d1d0b67bdd..ff1ad449f0 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -368,27 +368,25 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields, userIdR const permit = operation[entity]; if (operationKey === 'ACL') { - if (permit === true) { + if (Object.prototype.toString.call(permit) !== '[object Object]') { throw new Parse.Error( Parse.Error.INVALID_JSON, - `'${ - permit ? 'true' : 'false' - }' is not a valid value for class level permissions ${operationKey}:${entity}` + `'${permit}' is not a valid value for class level permissions acl` ); } - if (Object.prototype.toString.call(permit) !== '[object Object]') { + 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, - `'${permit}' is not a valid value for class level permissions ${operationKey}:${entity}:${permit}` + `'${invalidKeys.join(',')}' is not a valid key for class level permissions acl` ); } - const isValid = - Object.keys(permit).some(key => !['read', 'write'].includes(key)) || - Object.values(permit).some(key => typeof key !== Boolean); - if (!isValid) { + + if (invalidValues.length) { throw new Parse.Error( Parse.Error.INVALID_JSON, - `'${permit}' is not a valid value for class level permissions ${operationKey}:${entity}:${permit}` + `'${invalidValues.join(',')}' is not a valid value for class level permissions acl` ); } } else if (permit !== true) { From bc2cc2393c86e9ccde7184cad7f0c44092ee65d6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Mar 2025 19:56:29 +1100 Subject: [PATCH 08/10] Update SchemaController.js --- src/Controllers/SchemaController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ff1ad449f0..4b0f40b136 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -375,7 +375,7 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields, userIdR ); } const invalidKeys = Object.keys(permit).filter(key => !['read', 'write'].includes(key)); - const invalidValues = Object.values(permit).filter(key => typeof key !== Boolean); + const invalidValues = Object.values(permit).filter(key => typeof key !== 'boolean'); if (invalidKeys.length) { throw new Parse.Error( Parse.Error.INVALID_JSON, From 9ad5261d513f8f69bb24d053bb31587909182ec1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Mar 2025 20:05:52 +1100 Subject: [PATCH 09/10] fix tests --- spec/schemas.spec.js | 2 +- src/Controllers/SchemaController.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index cf0801418c..c25a457299 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2143,7 +2143,7 @@ describe('schemas', () => { }, }).then(fail, response => { expect(response.data.error).toEqual( - `'1' is not a valid value for class level permissions acl` + `'' is not a valid value for class level permissions acl find:*` ); done(); }); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 4b0f40b136..fccadd23ce 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -392,7 +392,7 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields, userIdR } else if (permit !== true) { throw new Parse.Error( 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}` ); } } From 496ad4c6a85a25c9d783b8b675e59ec0d4dc4973 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Mar 2025 20:21:14 +1100 Subject: [PATCH 10/10] Update schemas.spec.js --- spec/schemas.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index c25a457299..140dd405ec 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -2064,7 +2064,7 @@ describe('schemas', () => { }, }).then(fail, response => { 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(); });