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.
This commit is contained in:
Sandro Santilli 2013-11-12 23:18:40 +01:00
parent c7494c3c73
commit baa95a62d1
8 changed files with 112 additions and 24 deletions

View File

@ -1,9 +1,9 @@
1.5.0 -- 2013-MM-DD 1.5.0 -- 2013-MM-DD
------------------- -------------------
* Add support for specifying database connection passwords * Add support for configuring database connection passwords
* Add support for reading user-specific database_host from redis, * Optionally read user-specific database_host and database_password
as per CartoDB-2.5.0 model (#88) from redis as per CartoDB-2.5.0 model (#88, #89)
* Do not force ending dot in SQL-API hostname, for easier testing * Do not force ending dot in SQL-API hostname, for easier testing
1.4.1 -- 2013-11-08 1.4.1 -- 2013-11-08

View File

@ -11,7 +11,8 @@ var config = {
,cache_enabled: false ,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])' ,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_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: { ,postgres: {
// Parameters to pass to datasource plugin of mapnik // Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS // See http://github.com/mapnik/mapnik/wiki/PostGIS

View File

@ -11,7 +11,8 @@ var config = {
,cache_enabled: true ,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])' ,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_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: { ,postgres: {
// Parameters to pass to datasource plugin of mapnik // Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS // See http://github.com/mapnik/mapnik/wiki/PostGIS

View File

@ -11,7 +11,8 @@ var config = {
,cache_enabled: true ,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]' ,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_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: { ,postgres: {
// Parameters to pass to datasource plugin of mapnik // Parameters to pass to datasource plugin of mapnik
// See http://github.com/mapnik/mapnik/wiki/PostGIS // See http://github.com/mapnik/mapnik/wiki/PostGIS

View File

@ -11,6 +11,7 @@ var config = {
,cache_enabled: false ,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])' ,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 %>' ,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_auth_pass: 'test_cartodb_user_<%= user_id %>_pass'
,postgres: { ,postgres: {
// Parameters to pass to datasource plugin of mapnik // Parameters to pass to datasource plugin of mapnik

View File

@ -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 * Check the user map key for this particular subdomain/username
* *
@ -161,27 +185,49 @@ module.exports = function() {
}, },
function checkIfInternal(err, check_result){ function checkIfInternal(err, check_result){
if (err) throw err; if (err) throw err;
if (check_result === 1) {
// if unauthorized continue to check table privacy
if (check_result !== 1) return true;
// authorized by key, login as db owner // authorized by key, login as db owner
that.getId(req, function(err, user_id) { var user_params = {};
if (err) { callback(err); return; } var auth_user = global.settings.postgres_auth_user;
var dbuser = _.template(global.settings.postgres_auth_user, {user_id: user_id}); 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}); _.extend(req.params, {dbuser:dbuser});
if ( global.settings.postgres_auth_pass ) {
var dbpass = _.template(global.settings.postgres_auth_pass, {user_id: user_id}); // 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}); _.extend(req.params, {dbpassword:dbpass});
} }
callback(err, true); return true;
}); },
} else { function finish(err) {
return true; // continue to check if the table is public/private callback(err, true); // authorized (or error)
} }
);
} }
,function (err, data){ ,function getDatabase(err, data){
if (err) throw err; if (err) throw err;
that.getDatabase(req, this); that.getDatabase(req, this);
}, },
function(err, data){ function getPrivacy(err, data){
if (err) throw err; if (err) throw err;
var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table}); var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table});

View File

@ -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){ 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'}); var style = querystring.stringify({style: test_style_black_210, style_version: '2.1.0'});
assert.response(server, { assert.response(server, {

View File

@ -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_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_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 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}"':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 echo 'HSET rails:'"${TEST_DB}"':test_table_private_1 privacy "0"' | redis-cli -p ${REDIS_PORT} -n 0