Skip to content

Commit d559cb2

Browse files
authored
Move transform acl (#2021)
* Move ACL transforming into Parse Server For the database adapters, it will be more performant and easier to work with _rperm and _wperm than with the ACL object. This way we can type it as an array and so on, and once we have stronger validations in Parse Server, we can type it as an array containing strings of length < x, which will be much much better in sql databases. * Use destructuring
1 parent 5baa53d commit d559cb2

File tree

6 files changed

+106
-113
lines changed

6 files changed

+106
-113
lines changed

spec/MongoStorageAdapter.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe('MongoStorageAdapter', () => {
5454
.then(results => {
5555
expect(results.length).toEqual(1);
5656
var obj = results[0];
57-
expect(typeof obj._id).toEqual('string');
57+
expect(obj._id).toEqual('abcde');
5858
expect(obj.objectId).toBeUndefined();
5959
done();
6060
});

spec/MongoTransform.spec.js

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,9 @@ describe('parseObjectToMongoObjectForCreate', () => {
5858
done();
5959
});
6060

61-
it('basic ACL', (done) => {
61+
it('Doesnt allow ACL, as Parse Server should tranform ACL to _wperm + _rperm', done => {
6262
var input = {ACL: {'0123': {'read': true, 'write': true}}};
63-
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
64-
// This just checks that it doesn't crash, but it should check format.
63+
expect(() => transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} })).toThrow();
6564
done();
6665
});
6766

@@ -220,28 +219,10 @@ describe('transform schema key changes', () => {
220219
done();
221220
});
222221

223-
it('changes ACL storage to _rperm and _wperm', (done) => {
224-
var input = {
225-
ACL: {
226-
"*": { "read": true },
227-
"Kevin": { "write": true }
228-
}
229-
};
230-
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
231-
expect(typeof output._rperm).toEqual('object');
232-
expect(typeof output._wperm).toEqual('object');
233-
expect(output.ACL).toBeUndefined();
234-
expect(output._rperm[0]).toEqual('*');
235-
expect(output._wperm[0]).toEqual('Kevin');
236-
done();
237-
});
238-
239222
it('writes the old ACL format in addition to rperm and wperm', (done) => {
240223
var input = {
241-
ACL: {
242-
"*": { "read": true },
243-
"Kevin": { "write": true }
244-
}
224+
_rperm: ['*'],
225+
_wperm: ['Kevin'],
245226
};
246227

247228
var output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} });
@@ -257,11 +238,9 @@ describe('transform schema key changes', () => {
257238
_wperm: ["Kevin"]
258239
};
259240
var output = transform.mongoObjectToParseObject(null, input, { fields: {} });
260-
expect(typeof output.ACL).toEqual('object');
261-
expect(output._rperm).toBeUndefined();
262-
expect(output._wperm).toBeUndefined();
263-
expect(output.ACL['*']['read']).toEqual(true);
264-
expect(output.ACL['Kevin']['write']).toEqual(true);
241+
expect(output._rperm).toEqual(['*']);
242+
expect(output._wperm).toEqual(['Kevin']);
243+
expect(output.ACL).toBeUndefined()
265244
done();
266245
});
267246

src/Adapters/Storage/Mongo/MongoCollection.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,6 @@ export default class MongoCollection {
4444
return this._mongoCollection.count(query, { skip, limit, sort });
4545
}
4646

47-
// Atomically finds and updates an object based on query.
48-
// The result is the promise with an object that was in the database !AFTER! changes.
49-
// Postgres Note: Translates directly to `UPDATE * SET * ... RETURNING *`, which will return data after the change is done.
50-
findOneAndUpdate(query, update) {
51-
// arguments: query, sort, update, options(optional)
52-
// Setting `new` option to true makes it return the after document, not the before one.
53-
return this._mongoCollection.findAndModify(query, [], update, { new: true }).then(document => {
54-
// Value is the object where mongo returns multiple fields.
55-
return document.value;
56-
});
57-
}
58-
5947
insertOne(object) {
6048
return this._mongoCollection.insertOne(object);
6149
}

src/Adapters/Storage/Mongo/MongoStorageAdapter.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,14 @@ export class MongoStorageAdapter {
212212
.then(collection => collection.updateMany(mongoWhere, mongoUpdate));
213213
}
214214

215-
// Hopefully we can get rid of this in favor of updateObjectsByQuery.
215+
// Atomically finds and updates an object based on query.
216+
// Resolve with the updated object.
216217
findOneAndUpdate(className, query, schema, update) {
217218
const mongoUpdate = transformUpdate(className, update, schema);
218219
const mongoWhere = transformWhere(className, query, schema);
219220
return this.adaptiveCollection(className)
220-
.then(collection => collection.findOneAndUpdate(mongoWhere, mongoUpdate));
221+
.then(collection => collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { new: true }))
222+
.then(result => result.value);
221223
}
222224

223225
// Hopefully we can get rid of this. It's only used for config and hooks.

src/Adapters/Storage/Mongo/MongoTransform.js

Lines changed: 44 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,12 @@ const parseObjectKeyValueToMongoObjectKeyValue = (className, restKey, restValue,
273273

274274
// Main exposed method to create new objects.
275275
// restCreate is the "create" clause in REST API form.
276-
function parseObjectToMongoObjectForCreate(className, restCreate, schema) {
276+
const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => {
277277
if (className == '_User') {
278278
restCreate = transformAuthData(restCreate);
279279
}
280-
var mongoCreate = transformACL(restCreate);
280+
restCreate = addLegacyACL(restCreate);
281+
let mongoCreate = {}
281282
for (let restKey in restCreate) {
282283
let { key, value } = parseObjectKeyValueToMongoObjectKeyValue(
283284
className,
@@ -298,18 +299,18 @@ const transformUpdate = (className, restUpdate, parseFormatSchema) => {
298299
restUpdate = transformAuthData(restUpdate);
299300
}
300301

301-
var mongoUpdate = {};
302-
var acl = transformACL(restUpdate);
303-
if (acl._rperm || acl._wperm || acl._acl) {
304-
mongoUpdate['$set'] = {};
302+
let mongoUpdate = {};
303+
let acl = addLegacyACL(restUpdate)._acl;
304+
if (acl) {
305+
mongoUpdate.$set = {};
305306
if (acl._rperm) {
306-
mongoUpdate['$set']['_rperm'] = acl._rperm;
307+
mongoUpdate.$set._rperm = acl._rperm;
307308
}
308309
if (acl._wperm) {
309-
mongoUpdate['$set']['_wperm'] = acl._wperm;
310+
mongoUpdate.$set._wperm = acl._wperm;
310311
}
311312
if (acl._acl) {
312-
mongoUpdate['$set']['_acl'] = acl._acl;
313+
mongoUpdate.$set._acl = acl._acl;
313314
}
314315
}
315316
for (var restKey in restUpdate) {
@@ -347,67 +348,35 @@ function transformAuthData(restObject) {
347348
return restObject;
348349
}
349350

350-
// Transforms a REST API formatted ACL object to our two-field mongo format.
351-
// This mutates the restObject passed in to remove the ACL key.
352-
function transformACL(restObject) {
353-
var output = {};
354-
if (!restObject['ACL']) {
355-
return output;
356-
}
357-
var acl = restObject['ACL'];
358-
var rperm = [];
359-
var wperm = [];
360-
var _acl = {}; // old format
361-
362-
for (var entry in acl) {
363-
if (acl[entry].read) {
364-
rperm.push(entry);
365-
_acl[entry] = _acl[entry] || {};
366-
_acl[entry]['r'] = true;
367-
}
368-
if (acl[entry].write) {
369-
wperm.push(entry);
370-
_acl[entry] = _acl[entry] || {};
371-
_acl[entry]['w'] = true;
372-
}
373-
}
374-
output._rperm = rperm;
375-
output._wperm = wperm;
376-
output._acl = _acl;
377-
delete restObject.ACL;
378-
return output;
379-
}
351+
// Add the legacy _acl format.
352+
const addLegacyACL = restObject => {
353+
let restObjectCopy = {...restObject};
354+
let _acl = {};
380355

381-
// Transforms a mongo format ACL to a REST API format ACL key
382-
// This mutates the mongoObject passed in to remove the _rperm/_wperm keys
383-
function untransformACL(mongoObject) {
384-
var output = {};
385-
if (!mongoObject['_rperm'] && !mongoObject['_wperm']) {
386-
return output;
387-
}
388-
var acl = {};
389-
var rperm = mongoObject['_rperm'] || [];
390-
var wperm = mongoObject['_wperm'] || [];
391-
rperm.map((entry) => {
392-
if (!acl[entry]) {
393-
acl[entry] = {read: true};
394-
} else {
395-
acl[entry]['read'] = true;
396-
}
397-
});
398-
wperm.map((entry) => {
399-
if (!acl[entry]) {
400-
acl[entry] = {write: true};
401-
} else {
402-
acl[entry]['write'] = true;
403-
}
404-
});
405-
output['ACL'] = acl;
406-
delete mongoObject._rperm;
407-
delete mongoObject._wperm;
408-
return output;
356+
if (restObject._wperm) {
357+
restObject._wperm.forEach(entry => {
358+
_acl[entry] = { w: true };
359+
});
360+
}
361+
362+
if (restObject._rperm) {
363+
restObject._rperm.forEach(entry => {
364+
if (!(entry in _acl)) {
365+
_acl[entry] = { r: true };
366+
} else {
367+
_acl[entry].r = true;
368+
}
369+
});
370+
}
371+
372+
if (Object.keys(_acl).length > 0) {
373+
restObjectCopy._acl = _acl;
374+
}
375+
376+
return restObjectCopy;
409377
}
410378

379+
411380
// A sentinel value that helper transformations return when they
412381
// cannot perform a transformation
413382
function CannotTransform() {}
@@ -752,7 +721,14 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => {
752721
return BytesCoder.databaseToJSON(mongoObject);
753722
}
754723

755-
var restObject = untransformACL(mongoObject);
724+
let restObject = {};
725+
if (mongoObject._rperm || mongoObject._wperm) {
726+
restObject._rperm = mongoObject._rperm || [];
727+
restObject._wperm = mongoObject._wperm || [];
728+
delete mongoObject._rperm;
729+
delete mongoObject._wperm;
730+
}
731+
756732
for (var key in mongoObject) {
757733
switch(key) {
758734
case '_id':

src/Controllers/DatabaseController.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ function addReadACL(query, acl) {
2424
return newQuery;
2525
}
2626

27+
// Transforms a REST API formatted ACL object to our two-field mongo format.
28+
const transformObjectACL = ({ ACL, ...result }) => {
29+
if (!ACL) {
30+
return result;
31+
}
32+
33+
result._wperm = [];
34+
result._rperm = [];
35+
36+
for (let entry in ACL) {
37+
if (ACL[entry].read) {
38+
result._rperm.push(entry);
39+
}
40+
if (ACL[entry].write) {
41+
result._wperm.push(entry);
42+
}
43+
}
44+
return result;
45+
}
46+
2747
const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token'];
2848
const validateQuery = query => {
2949
if (query.ACL) {
@@ -210,6 +230,7 @@ DatabaseController.prototype.update = function(className, query, update, {
210230
throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
211231
}
212232
}
233+
update = transformObjectACL(update);
213234
if (many) {
214235
return this.adapter.updateObjectsByQuery(className, query, schema, update);
215236
} else if (upsert) {
@@ -376,7 +397,7 @@ DatabaseController.prototype.destroy = function(className, query, { acl } = {})
376397
DatabaseController.prototype.create = function(className, object, { acl } = {}) {
377398
// Make a copy of the object, so we don't mutate the incoming data.
378399
let originalObject = object;
379-
object = deepcopy(object);
400+
object = transformObjectACL(object);
380401

381402
var isMaster = acl === undefined;
382403
var aclGroup = acl || [];
@@ -671,13 +692,40 @@ DatabaseController.prototype.find = function(className, query, {
671692
return this.adapter.count(className, query, schema);
672693
} else {
673694
return this.adapter.find(className, query, schema, { skip, limit, sort })
674-
.then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object)));
695+
.then(objects => objects.map(object => {
696+
object = untransformObjectACL(object);
697+
return filterSensitiveData(isMaster, aclGroup, className, object)
698+
}));
675699
}
676700
});
677701
});
678702
});
679703
};
680704

705+
// Transforms a Database format ACL to a REST API format ACL
706+
const untransformObjectACL = ({_rperm, _wperm, ...output}) => {
707+
if (_rperm || _wperm) {
708+
output.ACL = {};
709+
710+
(_rperm || []).forEach(entry => {
711+
if (!output.ACL[entry]) {
712+
output.ACL[entry] = { read: true };
713+
} else {
714+
output.ACL[entry]['read'] = true;
715+
}
716+
});
717+
718+
(_wperm || []).forEach(entry => {
719+
if (!output.ACL[entry]) {
720+
output.ACL[entry] = { write: true };
721+
} else {
722+
output.ACL[entry]['write'] = true;
723+
}
724+
});
725+
}
726+
return output;
727+
}
728+
681729
DatabaseController.prototype.deleteSchema = function(className) {
682730
return this.collectionExists(className)
683731
.then(exist => {

0 commit comments

Comments
 (0)