From 88daefe353b888b4802768ec48e00d2da0713c4b Mon Sep 17 00:00:00 2001 From: Sandro Santilli Date: Tue, 12 Nov 2013 23:18:40 +0100 Subject: [PATCH] Add support for reading user-specific database_password from redis This commits adds support for CartoDB-2.5.0 model. Closes #89. Change is backward compatible. --- NEWS.md | 6 +- config/environments/development.js.example | 3 +- config/environments/production.js.example | 3 +- config/environments/staging.js.example | 3 +- config/environments/test.js.example | 1 + lib/cartodb/carto_data.js | 82 +++++++++++++++++----- test/acceptance/server.js | 37 ++++++++++ test/support/prepare_db.sh | 1 + 8 files changed, 112 insertions(+), 24 deletions(-) diff --git a/NEWS.md b/NEWS.md index 2539e419..5c354a4b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,9 @@ 1.5.0 -- 2013-MM-DD ------------------- -* Add support for specifying database connection passwords -* Add support for reading user-specific database_host from redis, - as per CartoDB-2.5.0 model (#88) +* Add support for configuring database connection passwords +* Optionally read user-specific database_host and database_password + from redis as per CartoDB-2.5.0 model (#88, #89) * Do not force ending dot in SQL-API hostname, for easier testing 1.4.1 -- 2013-11-08 diff --git a/config/environments/development.js.example b/config/environments/development.js.example index 9d3e704a..8f57d986 100644 --- a/config/environments/development.js.example +++ b/config/environments/development.js.example @@ -11,7 +11,8 @@ var config = { ,cache_enabled: false ,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])' ,postgres_auth_user: 'development_cartodb_user_<%= user_id %>' - ,postgres_auth_pass: 'development_cartodb_user_<%= user_id %>_pass' + // Supported labels: 'user_id', 'user_password' (both read from redis) + ,postgres_auth_pass: '<%= user_password %>' ,postgres: { // Parameters to pass to datasource plugin of mapnik // See http://github.com/mapnik/mapnik/wiki/PostGIS diff --git a/config/environments/production.js.example b/config/environments/production.js.example index 2270aab5..4f63a919 100644 --- a/config/environments/production.js.example +++ b/config/environments/production.js.example @@ -11,7 +11,8 @@ var config = { ,cache_enabled: true ,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])' ,postgres_auth_user: 'cartodb_user_<%= user_id %>' - ,postgres_auth_pass: 'cartodb_user_<%= user_id %>_pass' + // Supported labels: 'user_id', 'user_password' (both read from redis) + ,postgres_auth_pass: '<%= user_password %>' ,postgres: { // Parameters to pass to datasource plugin of mapnik // See http://github.com/mapnik/mapnik/wiki/PostGIS diff --git a/config/environments/staging.js.example b/config/environments/staging.js.example index bd1537f8..5dd3a27b 100644 --- a/config/environments/staging.js.example +++ b/config/environments/staging.js.example @@ -11,7 +11,8 @@ var config = { ,cache_enabled: true ,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]' ,postgres_auth_user: 'cartodb_staging_user_<%= user_id %>' - ,postgres_auth_pass: 'cartodb_staging_user_<%= user_id %>_pass' + // Supported labels: 'user_id', 'user_password' (both read from redis) + ,postgres_auth_pass: '<%= user_password %>' ,postgres: { // Parameters to pass to datasource plugin of mapnik // See http://github.com/mapnik/mapnik/wiki/PostGIS diff --git a/config/environments/test.js.example b/config/environments/test.js.example index 1f5f1a79..2edf8452 100644 --- a/config/environments/test.js.example +++ b/config/environments/test.js.example @@ -11,6 +11,7 @@ var config = { ,cache_enabled: false ,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])' ,postgres_auth_user: 'test_cartodb_user_<%= user_id %>' + // Supported labels: 'user_id', 'user_password' (both read from redis) ,postgres_auth_pass: 'test_cartodb_user_<%= user_id %>_pass' ,postgres: { // Parameters to pass to datasource plugin of mapnik diff --git a/lib/cartodb/carto_data.js b/lib/cartodb/carto_data.js index bb1bcb06..fe093bfb 100644 --- a/lib/cartodb/carto_data.js +++ b/lib/cartodb/carto_data.js @@ -123,6 +123,30 @@ module.exports = function() { }); }; + /** + * Get the database password for this particular subdomain/username + * + * @param req - standard express req object. importantly contains host information + * @param callback + */ + me.getDatabasePassword= function(req, callback) { + // strip subdomain from header host + var username = me.userFromHostname(req.headers.host); + var redisKey = _.template(this.user_key, {username: username}); + + this.retrieve(this.user_metadata_db, redisKey, 'database_password', function(err, data) { + if ( err ) callback(err, null); + else { + if ( data === null ) { + /* database_password was introduced in cartodb-2.5.0, + * for older versions we'll just use configured password */ + //console.log("WARNING: missing " + username + "'s database_password in redis (try CARTODB/script/restore_redis)"); + } + callback(err, data); + } + }); + }; + /** * Check the user map key for this particular subdomain/username * @@ -161,27 +185,49 @@ module.exports = function() { }, function checkIfInternal(err, check_result){ if (err) throw err; - if (check_result === 1) { - // authorized by key, login as db owner - that.getId(req, function(err, user_id) { - if (err) { callback(err); return; } - var dbuser = _.template(global.settings.postgres_auth_user, {user_id: user_id}); - _.extend(req.params, {dbuser:dbuser}); - if ( global.settings.postgres_auth_pass ) { - var dbpass = _.template(global.settings.postgres_auth_pass, {user_id: user_id}); - _.extend(req.params, {dbpassword:dbpass}); - } - callback(err, true); - }); - } else { - return true; // continue to check if the table is public/private - } + + // if unauthorized continue to check table privacy + if (check_result !== 1) return true; + + // authorized by key, login as db owner + var user_params = {}; + var auth_user = global.settings.postgres_auth_user; + var auth_pass = global.settings.postgres_auth_pass; + Step( + function getId() { + that.getId(req, this); + }, + function(err, user_id) { + if (err) throw err; + user_params['user_id'] = user_id; + var dbuser = _.template(auth_user, user_params); + _.extend(req.params, {dbuser:dbuser}); + + // skip looking up user_password if postgres_auth_pass + // doesn't contain the "user_password" label + if ( ! auth_pass.match(/\buser_password\b/) ) return null; + + that.getDatabasePassword(req, this); + }, + function(err, user_password) { + if (err) throw err; + user_params['user_password'] = user_password; + if ( auth_pass ) { + var dbpass = _.template(auth_pass, user_params); + _.extend(req.params, {dbpassword:dbpass}); + } + return true; + }, + function finish(err) { + callback(err, true); // authorized (or error) + } + ); } - ,function (err, data){ + ,function getDatabase(err, data){ if (err) throw err; that.getDatabase(req, this); }, - function(err, data){ + function getPrivacy(err, data){ if (err) throw err; var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table}); @@ -189,7 +235,7 @@ module.exports = function() { }, function(err, data){ callback(err, data); - } + } ); }; diff --git a/test/acceptance/server.js b/test/acceptance/server.js index 7e09304e..a9a5b720 100644 --- a/test/acceptance/server.js +++ b/test/acceptance/server.js @@ -807,6 +807,43 @@ suite('server', function() { ); }); + // See https://github.com/CartoDB/Windshaft-cartodb/issues/XXX + test("get'ing a tile with a user-specific database password", function(done){ + var style = querystring.stringify({style: test_style_black_200, style_version: '2.0.0'}); + var backupDBPass = global.settings.postgres_auth_pass; + global.settings.postgres_auth_pass = '<%= user_password %>'; + Step ( + function() { + var next = this; + assert.response(server, { + headers: {host: 'cartodb250user'}, + url: '/tiles/test_table/15/16046/12354.png?' + + 'cache_buster=4.20&api_key=4321&' + style, + method: 'GET', + encoding: 'binary' + },{}, function(res){ + next(null, res); + }); + }, + function checkRes(err, res) { + assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body); + var ct = res.headers['content-type']; + assert.equal(ct, 'image/png'); + assert.imageEqualsFile(res.body, + './test/fixtures/test_table_15_16046_12354_styled_black.png', + 2, this); + }, + function checkImage(err, similarity) { + if (err) throw err; + return null + }, + function finish(err) { + global.settings.postgres_auth_pass = backupDBPass; + done(err); + } + ); + }); + test("get'ing a tile with url specified 2.1.0 style should return an expected tile", function(done){ var style = querystring.stringify({style: test_style_black_210, style_version: '2.1.0'}); assert.response(server, { diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index a9b1149c..40be5d92 100755 --- a/test/support/prepare_db.sh +++ b/test/support/prepare_db.sh @@ -73,6 +73,7 @@ echo "HSET rails:users:cartodb250user id ${TESTUSERID}" | redis-cli -p ${REDIS_P echo 'HSET rails:users:cartodb250user database_name "'${TEST_DB}'"' | redis-cli -p ${REDIS_PORT} -n 5 echo 'HSET rails:users:cartodb250user database_host "localhost"' | redis-cli -p ${REDIS_PORT} -n 5 echo 'HSET rails:users:cartodb250user database_password "'${TESTPASS}'"' | redis-cli -p ${REDIS_PORT} -n 5 +echo "HSET rails:users:cartodb250user map_key 4321" | redis-cli -p ${REDIS_PORT} -n 5 echo 'HSET rails:'"${TEST_DB}"':my_table infowindow "this, that, the other"' | redis-cli -p ${REDIS_PORT} -n 0 echo 'HSET rails:'"${TEST_DB}"':test_table_private_1 privacy "0"' | redis-cli -p ${REDIS_PORT} -n 0