From a256aad6106b7a88689b45930c40088460b05ae7 Mon Sep 17 00:00:00 2001 From: Michael Gregorio Date: Thu, 10 Aug 2017 10:10:56 +0800 Subject: [PATCH 1/4] make saving for related models respect acl rules --- index.js | 15 ++-- server/relational-upsert.js | 133 ++++++++++++++++++++++++------------ 2 files changed, 99 insertions(+), 49 deletions(-) diff --git a/index.js b/index.js index cfa502d4..3adae22b 100644 --- a/index.js +++ b/index.js @@ -277,6 +277,7 @@ function cms(loopbackApplication, options) { //for CMS custom services provide context to loopbackApplication relationalUpsert.setLoopBack(loopbackApplication); + relationalUpsert.setConfig(config); customSort.setLoopBack(loopbackApplication); aws.setConfig(config.private); @@ -350,13 +351,16 @@ function cms(loopbackApplication, options) { accessToken: token, model: data.__model, property: data.__id ? 'updateAttributes' : 'create', - modelId: data.__id || null + modelId: data.__id || null, + remotingContext: { + req: req + } }; function upsertData() { - relationalUpsert.upsert(data, function(error, response) { + relationalUpsert.upsert(data, context, function(error, response) { if (error) { - res.status(500).send(error); + res.status(error.status).send(error.message); } else { res.send(response); } @@ -367,7 +371,7 @@ function cms(loopbackApplication, options) { upsertData(); } else { ACL.checkAccessForContext(context, function(err, acl) { - if (err) { return res.status(500).send(err); } + if (err) return res.status(500).send(err); if (acl.permission === 'DENY') { return res.status(403).send('Forbidden'); } upsertData(); }); @@ -413,8 +417,7 @@ function cms(loopbackApplication, options) { validateToken(req, function(err, isValid) { if (err) { return res.status(500).send(err); } if (!isValid) { return res.status(403).send('Forbidden'); } - - aws.getS3Credentials(req.query["path"], req.query["fileType"], function(error, credentials) { + aws.getS3Credentials(req.query.path, req.query.fileType, req.query.acl, function(error, credentials) { if (error) { res.status(500).send(error); } diff --git a/server/relational-upsert.js b/server/relational-upsert.js index f3ad90b2..1c2f79a0 100644 --- a/server/relational-upsert.js +++ b/server/relational-upsert.js @@ -23,12 +23,11 @@ */ var app; +var cmsConfig; var inflection = require('inflection'); var RELATIONSHIP_SINGLE = "RELATIONSHIP_SINGLE"; var RELATIONSHIP_MANY = "RELATIONSHIP_MANY"; -var relationshipKeys = []; -var relationshipManyToManyKeys = []; /** * Performs a recursive upsert into the data source via loopback API calls @@ -40,7 +39,7 @@ var relationshipManyToManyKeys = []; * @param callback * The callback with [error, result] params */ -function upsert(data, callback) { +function upsert(data, context, callback) { var model = app.models[data.__model]; if (!model) { var message = "model not found in post body __model = '"+data.__model+"'"; @@ -51,8 +50,8 @@ function upsert(data, callback) { //Get all relationship keys by checking for nested objects var keys = Object.keys(data); - relationshipKeys = []; - relationshipManyToManyKeys = []; + var relationshipKeys = []; + var relationshipManyToManyKeys = []; for (var i in keys) { var relationshipKey = keys[i]; var relationshipData = data[relationshipKey]; @@ -66,7 +65,7 @@ function upsert(data, callback) { } } - start(model, data, function(error, result) { + start(model, data, relationshipKeys, relationshipManyToManyKeys, context, function(error, result) { callback(error, result); }); @@ -78,9 +77,10 @@ function upsert(data, callback) { * @param data * @param callback */ -function start(model, data, callback) { +function start(model, data, relationshipKeys, relationshipManyToManyKeys, context, callback) { var index = 0; - next(RELATIONSHIP_SINGLE, model, data, index, function(error, count) { + next(RELATIONSHIP_SINGLE, model, data, index, relationshipKeys, relationshipManyToManyKeys, context, function(error, count) { + if (error) return callback(error); //After inserting all one-to-many relationships, perform the primary model upsert model.upsert(data, function(error, result) { if (error) { @@ -93,12 +93,11 @@ function start(model, data, callback) { //After upserting main model data, process all many-to-many relationship data last index = 0; - next(RELATIONSHIP_MANY, model, data, index, function(error, count) { - callback(null, result); //finished upserting all relationship data and model data + next(RELATIONSHIP_MANY, model, data, index, relationshipKeys, relationshipManyToManyKeys, context, function(error, count) { + callback(error, result); //finished upserting all relationship data and model data }); } }); - }); } @@ -111,7 +110,7 @@ function start(model, data, callback) { * @param index * @param callback */ -function next(processRelationshipType, model, data, index, callback) { +function next(processRelationshipType, model, data, index, relationshipKeys, relationshipManyToManyKeys, context, callback) { var length = processRelationshipType == RELATIONSHIP_SINGLE ? relationshipKeys.length : relationshipManyToManyKeys.length; if (index >= length) { @@ -131,47 +130,91 @@ function next(processRelationshipType, model, data, index, callback) { if (!relationSettings) { console.warn("WARNING: no relationship found for relationshipKey = " + relationshipKey); index++; - next(processRelationshipType, model, data, index, callback); + next(processRelationshipType, model, data, index, relationshipKeys, relationshipManyToManyKeys, context, callback); return; } var relationshipModel = app.models[relationSettings.model]; if (!relationshipModel) { console.warn("WARNING: cannot resolve relationship model = " + relationSettings.model); index++; - next(processRelationshipType, model, data, index, callback); + next(processRelationshipType, model, data, index, relationshipKeys, relationshipManyToManyKeys, context, callback); return; } - if (processRelationshipType == RELATIONSHIP_SINGLE) { //upsert the one-to-many relationship model data before upserting main model data - relationshipModel.upsert(relationshipData, function(error, result) { - if (error) { - console.error(error); - callback(error); - } else { - var id = result[relationshipModel.getIdName()]; - //assign the FK ID back to main model - data[relationSettings.foreignKey] = id; - delete data[relationshipKey]; //make sure to remove relationship data from the main model (otherwise upsert won't work for the relationshipKey) - index++; - next(RELATIONSHIP_SINGLE, model, data, index, callback); - } - }); + function executeUpsert() { + relationshipModel.upsert(relationshipData, function(error, result) { + if (error) { + console.error(error); + callback(error); + } else { + var id = result[relationshipModel.getIdName()]; + //assign the FK ID back to main model + data[relationSettings.foreignKey] = id; + delete data[relationshipKey]; //make sure to remove relationship data from the main model (otherwise upsert won't work for the relationshipKey) + index++; + next(RELATIONSHIP_SINGLE, model, data, index, relationshipKeys, relationshipManyToManyKeys, context, callback); + } + }); + } + var id = relationshipModel.getIdName(); + var ctx = { + accessToken: context.accessToken.id, + model: relationSettings.model, + property: relationshipData[id] ? 'updateAttributes' : 'create', + modelId: relationshipData[id] || null, + remotingContext: context.remotingContext + }; + if (cmsConfig.public.isUnsafeUpsert) { + executeUpsert(); + } else { + app.models.ACL.checkAccessForContext(ctx, function(err, acl) { + if (err) return callback(err); + if (acl.permission === 'DENY') { + var error = new Error('Forbidden.'); + error.status = 403; + return callback(error); + } + executeUpsert(); + }); + } } else if (processRelationshipType == RELATIONSHIP_MANY) { //relationshipData is an Array of hasMany values (a many-to-many relationship) - upsertManyToMany(model, data, relationshipKey, relationshipData, relationSettings, function(error, result) { - if (error) { - console.error(error); - callback(error); - } else { - delete data[relationshipKey]; //make sure to remove relationship data from the main model (otherwise upsert won't work for the relationshipKey) - index++; - next(RELATIONSHIP_MANY, model, data, index, callback); - } - }); - + function executeUpsertManyToMany() { + upsertManyToMany(model, data, relationshipKey, relationshipData, relationSettings, function(error, result) { + if (error) { + console.error(error); + callback(error); + } else { + delete data[relationshipKey]; //make sure to remove relationship data from the main model (otherwise upsert won't work for the relationshipKey) + index++; + next(RELATIONSHIP_MANY, model, data, index, relationshipKeys, relationshipManyToManyKeys, context, callback); + } + }); + } + + var id = relationshipModel.getIdName(); + var ctx = { + accessToken: context.accessToken, + model: relationSettings.model, + property: relationshipData[id] ? 'updateAttributes' : 'create', + modelId: relationshipData[id] || null, + remotingContext: context.remotingContext + }; + if (cmsConfig.public.isUnsafeUpsert) { + executeUpsertManyToMany(); + } else { + app.models.ACL.checkAccessForContext(ctx, function(err, acl) { + if (err) return callback(err); + if (acl.permission === 'DENY') { + var error = new Error('Forbidden.'); + error.status = 403; + return callback(error); + } + executeUpsertManyToMany(); + }); + } } - } /** @@ -324,9 +367,13 @@ function nextManyToMany(junctionModel, junctionModelIdKey, junctionRelationIdKey } } - -module.exports.setLoopBack = function(loopback) { - app = loopback; +module.exports = { + setLoopBack: function(loopback) { + app = loopback; + }, + setConfig: function(config) { + cmsConfig = config; + } }; module.exports.upsert = upsert; From b48d413b1d5219e1926b8d804bf1692dc6d93ef4 Mon Sep 17 00:00:00 2001 From: Michael Gregorio Date: Thu, 10 Aug 2017 10:20:23 +0800 Subject: [PATCH 2/4] fix access for accesstoken object --- server/relational-upsert.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/relational-upsert.js b/server/relational-upsert.js index 1c2f79a0..b8f0bc92 100644 --- a/server/relational-upsert.js +++ b/server/relational-upsert.js @@ -159,7 +159,7 @@ function next(processRelationshipType, model, data, index, relationshipKeys, rel } var id = relationshipModel.getIdName(); var ctx = { - accessToken: context.accessToken.id, + accessToken: context.accessToken, model: relationSettings.model, property: relationshipData[id] ? 'updateAttributes' : 'create', modelId: relationshipData[id] || null, From 857ad2e561e46dcce1a0bed76ca11b258aab0075 Mon Sep 17 00:00:00 2001 From: Michael Gregorio Date: Fri, 25 Aug 2017 14:47:12 +0800 Subject: [PATCH 3/4] add custom boolean flag per model to check to properly handle the error --- server/relational-upsert.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/relational-upsert.js b/server/relational-upsert.js index b8f0bc92..9f92f188 100644 --- a/server/relational-upsert.js +++ b/server/relational-upsert.js @@ -170,7 +170,7 @@ function next(processRelationshipType, model, data, index, relationshipKeys, rel } else { app.models.ACL.checkAccessForContext(ctx, function(err, acl) { if (err) return callback(err); - if (acl.permission === 'DENY') { + if (acl.permission === 'DENY' && relationshipModel.settings.strictRelationalUpsert) { var error = new Error('Forbidden.'); error.status = 403; return callback(error); @@ -206,7 +206,7 @@ function next(processRelationshipType, model, data, index, relationshipKeys, rel } else { app.models.ACL.checkAccessForContext(ctx, function(err, acl) { if (err) return callback(err); - if (acl.permission === 'DENY') { + if (acl.permission === 'DENY' && relationshipModel.settings.strictRelationalUpsert) { var error = new Error('Forbidden.'); error.status = 403; return callback(error); From 1a815f4670e08f9189a520e93e21643e31ed189a Mon Sep 17 00:00:00 2001 From: Michael Gregorio Date: Tue, 29 Aug 2017 08:06:41 +0800 Subject: [PATCH 4/4] move condition inside deny to properly silent error --- server/relational-upsert.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/relational-upsert.js b/server/relational-upsert.js index 9f92f188..31d4f6ba 100644 --- a/server/relational-upsert.js +++ b/server/relational-upsert.js @@ -170,7 +170,8 @@ function next(processRelationshipType, model, data, index, relationshipKeys, rel } else { app.models.ACL.checkAccessForContext(ctx, function(err, acl) { if (err) return callback(err); - if (acl.permission === 'DENY' && relationshipModel.settings.strictRelationalUpsert) { + if (acl.permission === 'DENY') { + if (!relationshipModel.settings.strictRelationalUpsert) return callback(); var error = new Error('Forbidden.'); error.status = 403; return callback(error); @@ -206,7 +207,8 @@ function next(processRelationshipType, model, data, index, relationshipKeys, rel } else { app.models.ACL.checkAccessForContext(ctx, function(err, acl) { if (err) return callback(err); - if (acl.permission === 'DENY' && relationshipModel.settings.strictRelationalUpsert) { + if (acl.permission === 'DENY') { + if (!relationshipModel.settings.strictRelationalUpsert) return callback(); var error = new Error('Forbidden.'); error.status = 403; return callback(error);