Datasource to give per-layer authentication in named layers

Make beforeLayergroupCreate to return a datasource with different
 authentication for the different layers.
 - Named layers will get access to private tables in case it's needed

Changes in MapConfigNamedLayersAdapter:
  - It will retrieve the dbAuth params only if named layers are present so
  there is no extra overhead for normal layers
  - Rename queue function signature from `callback` to `done` so it is easier
  to follow the code

Add several tests to validate `named` layers authentication
This commit is contained in:
Raul Ochoa
2015-02-04 11:30:36 +01:00
parent 58f9f5f7a8
commit 2c7bc6adde
7 changed files with 464 additions and 38 deletions
@@ -1,5 +1,6 @@
var queue = require('queue-async');
var _ = require('underscore');
var Datasource = require('windshaft').Datasource;
function MapConfigNamedLayersAdapter(templateMaps) {
this.templateMaps = templateMaps;
@@ -7,16 +8,16 @@ function MapConfigNamedLayersAdapter(templateMaps) {
module.exports = MapConfigNamedLayersAdapter;
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, callback) {
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
var self = this;
var adaptLayersQueue = queue(layers.length);
function adaptLayer(layer, callback) {
if (layer.type === 'named') {
function adaptLayer(layer, done) {
if (isNamedTypeLayer(layer)) {
if (!layer.options.name) {
return callback(new Error('Missing Named Map `name` in layer options'));
return done(new Error('Missing Named Map `name` in layer options'));
}
var templateName = layer.options.name;
@@ -25,7 +26,7 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal
self.templateMaps.getTemplate(username, templateName, function(err, template) {
if (err || !template) {
return callback(new Error("Template '" + templateName + "' of user '" + username + "' not found"));
return done(new Error("Template '" + templateName + "' of user '" + username + "' not found"));
}
if (self.templateMaps.isAuthorized(template, templateAuthTokens)) {
@@ -36,41 +37,84 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, cal
if (nestedNamedLayers.length > 0) {
var nestedNamedMapsError = new Error('Nested named layers are not allowed');
// nestedNamedMapsError.http_status = 400;
return callback(nestedNamedMapsError);
return done(nestedNamedMapsError);
}
try {
var templateLayergroupConfig = self.templateMaps.instance(template, templateConfigParams);
return callback(null, templateLayergroupConfig.layers);
return done(null, {
datasource: true,
layers: templateLayergroupConfig.layers
});
} catch (err) {
return callback(err);
return done(err);
}
} else {
var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation");
unauthorizedError.http_status = 403;
return callback(unauthorizedError);
return done(unauthorizedError);
}
});
} else {
return callback(null, layer);
return done(null, {
datasource: false,
layers: [layer]
});
}
}
layers.forEach(function(layer) {
adaptLayersQueue.defer(adaptLayer, layer);
});
var datasourceBuilder = new Datasource.Builder();
function layersAdaptQueueFinish(err, layers) {
function layersAdaptQueueFinish(err, layersResults) {
if (err) {
return callback(err);
}
if (!layers || layers.length === 0) {
if (!layersResults || layersResults.length === 0) {
return callback(new Error('Missing layers array from layergroup config'));
}
return callback(null, _.flatten(layers));
var layers = [],
currentLayerIndex = 0;
layersResults.forEach(function(layersResult) {
layersResult.layers.forEach(function(layer) {
layers.push(layer);
if (layersResult.datasource) {
datasourceBuilder.withLayerDatasource(currentLayerIndex, {
user: dbAuth.dbuser
});
}
currentLayerIndex++;
});
});
return callback(null, layers, datasourceBuilder.build());
}
var dbAuth = {};
if (_.some(layers, isNamedTypeLayer)) {
// Lazy load dbAuth
dbMetadata.setDBAuth(username, dbAuth, function(err) {
if (err) {
return callback(err);
}
layers.forEach(function(layer) {
adaptLayersQueue.defer(adaptLayer, layer);
});
adaptLayersQueue.awaitAll(layersAdaptQueueFinish);
});
} else {
return callback(null, layers, datasourceBuilder.build());
}
adaptLayersQueue.awaitAll(layersAdaptQueueFinish);
};
function isNamedTypeLayer(layer) {
return layer.type === 'named';
}
+2 -2
View File
@@ -307,13 +307,13 @@ module.exports = function(redisPool) {
};
me.beforeLayergroupCreate = function(req, requestMapConfig, callback) {
mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(this.userByReq(req), requestMapConfig.layers, this, function(err, layers, datasource) {
if (err) {
return callback(err);
}
requestMapConfig.layers = layers;
return callback(null, requestMapConfig)
return callback(null, requestMapConfig, datasource)
});
};
+6 -6
View File
@@ -97,7 +97,7 @@
},
"inherits": {
"version": "2.0.1",
"from": "inherits@~2.0.1"
"from": "inherits@2"
}
}
}
@@ -184,8 +184,8 @@
},
"windshaft": {
"version": "0.35.2",
"from": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step",
"resolved": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step",
"from": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource",
"resolved": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource",
"dependencies": {
"chronograph": {
"version": "0.1.0",
@@ -193,9 +193,9 @@
"resolved": "git://github.com/CartoDB/chronographjs.git#0b8c35eee510cfa14a16be24d70533b38ecc1d2d"
},
"grainstore": {
"version": "0.22.1",
"from": "https://github.com/CartoDB/grainstore/tarball/0.22.1",
"resolved": "https://github.com/CartoDB/grainstore/tarball/0.22.1",
"version": "0.22.2",
"from": "https://github.com/CartoDB/grainstore/tarball/datasource-extend",
"resolved": "https://github.com/CartoDB/grainstore/tarball/datasource-extend",
"dependencies": {
"carto": {
"version": "0.9.5-cdb2",
+1 -1
View File
@@ -25,7 +25,7 @@
"node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0",
"underscore" : "~1.6.0",
"dot": "~1.0.2",
"windshaft": "https://github.com/CartoDB/Windshaft/tarball/before-layergroup-step",
"windshaft": "https://github.com/CartoDB/Windshaft/tarball/mapconfig-per-layer-datasource",
"step": "~0.0.5",
"queue-async": "~1.0.7",
"request": "~2.9.203",
+72 -2
View File
@@ -109,7 +109,12 @@ suite('named_layers', function() {
if (err) {
return done(err);
}
sqlapiServer = new SQLAPIEmulator(global.environment.sqlapi.port, done);
templateMaps.addTemplate(username, template, function(err) {
if (err) {
return done(err);
}
sqlapiServer = new SQLAPIEmulator(global.environment.sqlapi.port, done);
});
});
});
});
@@ -442,6 +447,66 @@ suite('named_layers', function() {
});
test('should return 403 when private table is accessed from non named layer', function(done) {
var layergroup = {
version: '1.3.0',
layers: [
{
type: 'cartodb',
options: {
sql: 'select * from test_table_private_1',
cartocss: '#layer { marker-fill: #cc3300; }',
cartocss_version: '2.3.0'
}
},
{
type: 'named',
options: {
name: templateName
}
}
]
};
Step(
function createLayergroup() {
var next = this;
assert.response(server,
{
url: '/tiles/layergroup',
method: 'POST',
headers: {
host: 'localhost',
'Content-Type': 'application/json'
},
data: JSON.stringify(layergroup)
},
{
status: 403
},
function(res, err) {
next(err, res);
}
);
},
function checkLayergroup(err, response) {
if (err) {
throw err;
}
var parsedBody = JSON.parse(response.body);
assert.ok(parsedBody.errors[0].match(/permission denied for relation test_table_private_1/));
return null;
},
function finish(err) {
done(err);
}
);
});
suiteTeardown(function(done) {
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
@@ -452,7 +517,12 @@ suite('named_layers', function() {
if (err) {
return done(err);
}
sqlapiServer.close(done);
templateMaps.delTemplate(username, templateName, function(err) {
if (err) {
return done(err);
}
sqlapiServer.close(done);
});
});
});
});
@@ -0,0 +1,297 @@
var test_helper = require('../support/test_helper');
var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');
var Step = require('step');
var _ = require('underscore');
// configure redis pool instance to use in tests
var redisPool = RedisPool(global.environment.redis);
var templateMaps = new TemplateMaps(redisPool, {
max_user_templates: global.environment.maxUserTemplates
});
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
var wadusSql = 'select 1 wadusLayer, null::geometry the_geom_webmercator';
var wadusLayer = {
type: 'cartodb',
options: {
sql: wadusSql,
cartocss: '#layer { marker-fill: black; }',
cartocss_version: '2.3.0'
}
};
var wadusTemplateSql = 'select 1 wadusTemplateLayer, null::geometry the_geom_webmercator';
var wadusTemplateLayer = {
type: 'cartodb',
options: {
sql: wadusTemplateSql,
cartocss: '#layer { marker-fill: <%= color %>; }',
cartocss_version: '2.3.0'
}
};
var wadusMapnikSql = 'select 1 wadusMapnikLayer, null::geometry the_geom_webmercator';
var wadusMapnikLayer = {
type: 'mapnik',
options: {
sql: wadusMapnikSql,
cartocss: '#layer { polygon-fill: <%= polygon_color %>; }',
cartocss_version: '2.3.0'
}
};
var username = 'localhost';
var templateName = 'valid_template';
var template = {
version: '0.0.1',
name: templateName,
auth: {
method: 'open'
},
"placeholders": {
"color": {
"type": "css_color",
"default": "#cc3300"
}
},
layergroup: {
layers: [
wadusTemplateLayer
]
}
};
var multipleLayersTemplateName = 'multiple_valid_template';
var multipleLayersTemplate = {
version: '0.0.1',
name: multipleLayersTemplateName,
auth: {
method: 'token',
valid_tokens: ['valid1', 'valid2']
},
"placeholders": {
"polygon_color": {
"type": "css_color",
"default": "green"
},
"color": {
"type": "css_color",
"default": "red"
}
},
layergroup: {
layers: [
wadusMapnikLayer,
wadusTemplateLayer
]
}
};
suite('named_layers datasources', function() {
suiteSetup(function(done) {
templateMaps.addTemplate(username, template, function(err) {
if (err) {
return done(err);
}
templateMaps.addTemplate(username, multipleLayersTemplate, done);
});
});
function makeNamedMapLayerConfig(layers) {
return {
version: '1.3.0',
layers: layers
};
}
var simpleNamedLayer = {
type: 'named',
options: {
name: templateName
}
};
var multipleLayersNamedLayer = {
type: 'named',
options: {
name: multipleLayersTemplateName,
auth_tokens: ['valid2']
}
};
var testScenarios = [
{
desc: 'without datasource for non-named layers',
config: makeNamedMapLayerConfig([wadusLayer]),
test: function(err, layers, datasource, done) {
assert.ok(!err);
assert.equal(layers.length, 1);
assert.equal(layers[0].type, 'cartodb');
assert.equal(layers[0].options.sql, wadusSql);
assert.equal(datasource.getLayerDatasource(0), undefined);
done();
}
},
{
desc: 'with datasource for the named layer but not for the normal',
config: makeNamedMapLayerConfig([wadusLayer, simpleNamedLayer]),
test: function(err, layers, datasource, done) {
assert.ok(!err);
assert.equal(layers.length, 2);
assert.equal(layers[0].type, 'cartodb');
assert.equal(layers[0].options.sql, wadusSql);
assert.equal(datasource.getLayerDatasource(0), undefined);
assert.equal(layers[1].type, 'cartodb');
assert.equal(layers[1].options.sql, wadusTemplateSql);
var layerDatasource = datasource.getLayerDatasource(1);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
done();
}
},
{
desc: 'with datasource for the multiple layers in the named but not for the normal',
config: makeNamedMapLayerConfig([wadusLayer, multipleLayersNamedLayer]),
test: function(err, layers, datasource, done) {
assert.ok(!err);
assert.equal(layers.length, 3);
assert.equal(layers[0].type, 'cartodb');
assert.equal(layers[0].options.sql, wadusSql);
assert.equal(datasource.getLayerDatasource(0), undefined);
assert.equal(layers[1].type, 'mapnik');
assert.equal(layers[1].options.sql, wadusMapnikSql);
var layerDatasource = datasource.getLayerDatasource(1);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[2].type, 'cartodb');
assert.equal(layers[2].options.sql, wadusTemplateSql);
layerDatasource = datasource.getLayerDatasource(2);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
done();
}
},
{
desc: 'all with datasource because all are named',
config: makeNamedMapLayerConfig([multipleLayersNamedLayer, simpleNamedLayer]),
test: function(err, layers, datasource, done) {
assert.ok(!err);
assert.equal(layers.length, 3);
assert.equal(layers[0].type, 'mapnik');
assert.equal(layers[0].options.sql, wadusMapnikSql);
var layerDatasource = datasource.getLayerDatasource(0);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[1].type, 'cartodb');
assert.equal(layers[1].options.sql, wadusTemplateSql);
layerDatasource = datasource.getLayerDatasource(1);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[2].type, 'cartodb');
assert.equal(layers[2].options.sql, wadusTemplateSql);
layerDatasource = datasource.getLayerDatasource(2);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
done();
}
},
{
desc: 'with a mix of datasource and no datasource depending if layers are named or not',
config: makeNamedMapLayerConfig([simpleNamedLayer, multipleLayersNamedLayer, wadusLayer, simpleNamedLayer, wadusLayer, multipleLayersNamedLayer]),
test: function(err, layers, datasource, done) {
assert.ok(!err);
assert.equal(layers.length, 8);
assert.equal(layers[0].type, 'cartodb');
assert.equal(layers[0].options.sql, wadusTemplateSql);
var layerDatasource = datasource.getLayerDatasource(0);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[1].type, 'mapnik');
assert.equal(layers[1].options.sql, wadusMapnikSql);
layerDatasource = datasource.getLayerDatasource(1);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[2].type, 'cartodb');
assert.equal(layers[2].options.sql, wadusTemplateSql);
layerDatasource = datasource.getLayerDatasource(2);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[3].type, 'cartodb');
assert.equal(layers[3].options.sql, wadusSql);
assert.equal(datasource.getLayerDatasource(3), undefined);
assert.equal(layers[4].type, 'cartodb');
assert.equal(layers[4].options.sql, wadusTemplateSql);
layerDatasource = datasource.getLayerDatasource(4);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[5].type, 'cartodb');
assert.equal(layers[5].options.sql, wadusSql);
assert.equal(datasource.getLayerDatasource(5), undefined);
assert.equal(layers[6].type, 'mapnik');
assert.equal(layers[6].options.sql, wadusMapnikSql);
layerDatasource = datasource.getLayerDatasource(6);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
assert.equal(layers[7].type, 'cartodb');
assert.equal(layers[7].options.sql, wadusTemplateSql);
layerDatasource = datasource.getLayerDatasource(7);
assert.notEqual(layerDatasource, undefined);
assert.ok(layerDatasource.user);
done();
}
}
];
testScenarios.forEach(function(testScenario) {
test('should return a list of layers ' + testScenario.desc, function(done) {
mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, serverOptions, function(err, layers, datasource) {
testScenario.test(err, layers, datasource, done);
});
});
});
suiteTeardown(function(done) {
templateMaps.delTemplate(username, templateName, function(err) {
if (err) {
return done(err);
}
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
});
});
});
@@ -1,9 +1,11 @@
var testHelper = require('../support/test_helper');
var assert = require('assert');
var RedisPool = require('redis-mpool');
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');
var test_helper = require('../support/test_helper');
var Step = require('step');
var _ = require('underscore');
@@ -36,7 +38,7 @@ suite('mapconfig_named_layers_adapter', function() {
}
};
var username = 'me';
var username = 'localhost';
var templateName = 'valid_template';
var template = {
@@ -143,10 +145,12 @@ suite('mapconfig_named_layers_adapter', function() {
var missingNamedMapLayerConfig = makeNamedMapLayerConfig({
config: {}
});
mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(err);
assert.ok(!layers);
assert.ok(!datasource);
assert.equal(err.message, 'Missing Named Map `name` in layer options');
done();
});
});
@@ -156,10 +160,12 @@ suite('mapconfig_named_layers_adapter', function() {
var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({
name: missingTemplateName
});
mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(err);
assert.ok(!layers);
assert.ok(!datasource);
assert.equal(err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found");
done();
});
});
@@ -173,9 +179,10 @@ suite('mapconfig_named_layers_adapter', function() {
var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
name: tokenAuthTemplateName
});
mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(err);
assert.ok(!layers);
assert.ok(!datasource);
assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation");
templateMaps.delTemplate(username, tokenAuthTemplateName, done);
@@ -192,9 +199,10 @@ suite('mapconfig_named_layers_adapter', function() {
var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({
name: nestedNamedMapTemplateName
});
mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(err);
assert.ok(!layers);
assert.ok(!datasource);
assert.equal(err.message, 'Nested named layers are not allowed');
templateMaps.delTemplate(username, nestedNamedMapTemplateName, done);
@@ -206,10 +214,12 @@ suite('mapconfig_named_layers_adapter', function() {
var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({
name: templateName
});
mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(!err);
assert.ok(layers.length, 1);
assert.ok(layers[0].type, 'cartodb');
assert.notEqual(datasource.getLayerDatasource(0), undefined);
done();
});
});
@@ -224,9 +234,10 @@ suite('mapconfig_named_layers_adapter', function() {
name: tokenAuthTemplateName,
auth_tokens: ['valid1']
});
mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(!err);
assert.equal(layers.length, 1);
assert.notEqual(datasource.getLayerDatasource(0), undefined);
templateMaps.delTemplate(username, tokenAuthTemplateName, done);
});
@@ -243,15 +254,17 @@ suite('mapconfig_named_layers_adapter', function() {
name: multipleLayersTemplateName,
auth_tokens: ['valid2']
});
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(!err);
assert.equal(layers.length, 2);
assert.equal(layers[0].type, 'mapnik');
assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }');
assert.notEqual(datasource.getLayerDatasource(0), undefined);
assert.equal(layers[1].type, 'cartodb');
assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }');
assert.notEqual(datasource.getLayerDatasource(1), undefined);
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
});
@@ -275,15 +288,17 @@ suite('mapconfig_named_layers_adapter', function() {
},
auth_tokens: ['valid2']
});
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, function(err, layers) {
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, serverOptions, function(err, layers, datasource) {
assert.ok(!err);
assert.equal(layers.length, 2);
assert.equal(layers[0].type, 'mapnik');
assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }');
assert.notEqual(datasource.getLayerDatasource(0), undefined);
assert.equal(layers[1].type, 'cartodb');
assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }');
assert.notEqual(datasource.getLayerDatasource(1), undefined);
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
});