Delegate user permission to PostgreSQL (closes #18)

If the request is authenticated (with map_key) then we log as the
database owner, otherwise we log as the default user.
The default user is now "publicuser" by default.

Raises dependency on Windshaft to 0.4.9+, to get the grainstore
version allowing override of database username.

Add test for req2params function, particularly authentication,
Add test for authenticated / unauthenticated access
This commit is contained in:
Sandro Santilli 2012-07-18 11:00:24 +02:00
parent c918b09e64
commit de275bfc50
13 changed files with 150 additions and 36 deletions

View File

@ -6,7 +6,8 @@ var config = {
,cache_enabled: false
,postgres: {
type: "postgis",
user: "tileuser",
user: "publicuser",
db_user: 'development_cartodb_user_<%= user_id %>',
host: '127.0.0.1',
port: 5432,
extent: "-20005048.4188,-20005048.4188,20005048.4188,20005048.4188",

View File

@ -5,7 +5,8 @@ var config = {
,enable_cors: true
,cache_enabled: true
,postgres: {
user: "tileuser",
user: "publicuser",
db_user: 'cartodb_user_<%= user_id %>',
host: '127.0.0.1',
port: 6432,
extent: "-20005048.4188,-20005048.4188,20005048.4188,20005048.4188",

View File

@ -5,7 +5,8 @@ var config = {
,enable_cors: true
,cache_enabled: false
,postgres: {
user: "tileuser",
user: "publicuser",
db_user: 'test_cartodb_user_<%= user_id %>',
host: '127.0.0.1',
port: 5432,
srid: 4326,

View File

@ -82,26 +82,18 @@ module.exports = function() {
},
function checkIfInternal(err, check_result){
if (err) throw err;
if (check_result === 1){
callback(err, true); // Internal access so early exit with access.
if (check_result === 1) {
// authorized by key, login as db owner
that.getId(req, function(err, user_id) {
if (err) throw new Error(err);
var dbuser = _.template(global.settings.postgres.db_user, {user_id: user_id});
_.extend(req.params, {dbuser:dbuser});
callback(err, true);
});
} else {
return true; // continue to check if the table is public/private
// log to db as unprivileged user
callback(err, true);
}
},
function (err, data){
if (err) throw err;
that.getDatabase(req, this);
},
function(err, data){
if (err) throw err;
var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table});
that.retrieve(that.table_metadata_db, redisKey, 'privacy', this);
},
function(err, data){
if (err) throw err;
callback(err, data);
}
);
};

View File

@ -22,7 +22,8 @@
"cluster": "0.6.4",
"node-varnish": "0.1.1",
"underscore" : "1.1.x",
"windshaft" : "0.4.8",
"grainstore" : "~0.3.0",
"windshaft" : "~0.4.9",
"step": "0.0.x",
"generic-pool": "1.0.x",
"redis": "0.6.7",

View File

@ -34,6 +34,7 @@ PATH=node_modules/.bin/:$PATH
echo "Running tests"
mocha -u tdd \
test/unit/cartodb/redis_pool.test.js \
test/unit/cartodb/req2params.test.js \
test/acceptance/cache_validator.js \
test/acceptance/server.js

View File

@ -1,6 +1,6 @@
var assert = require('../support/assert');
var net = require('net');
require(__dirname + '/../test_helper');
require(__dirname + '/../support/test_helper');
var CacheValidator = require(__dirname + '/../../lib/cartodb/cache_validator');
var tests = module.exports = {};

View File

@ -2,7 +2,7 @@ var assert = require('../support/assert');
var tests = module.exports = {};
var _ = require('underscore');
var querystring = require('querystring');
require(__dirname + '/../test_helper');
require(__dirname + '/../support/test_helper');
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
@ -199,5 +199,32 @@ suite('server', function() {
}, function() { done(); });
});
test("get'ing a tile with data from private table should succeed when authenticated", function(done){
// NOTE: may fail if grainstore < 0.3.0 is used by Windshaft
var sql = querystring.stringify({sql: "SELECT * FROM test_table_private_1", map_key: 1234})
assert.response(server, {
headers: {host: 'vizzuality.localhost.lan'},
url: '/tiles/gadm4/6/31/24.png?' + sql,
method: 'GET'
},{
status: 200,
headers: { 'Content-Type': 'image/png' }
}, function() { done(); });
});
test("get'ing a tile with data from private table should fail when unauthenticated", function(done){
var sql = querystring.stringify({
sql: "SELECT * FROM test_table_private_1",
cache_buster:2 // this is to avoid getting the cached response
});
assert.response(server, {
headers: {host: 'vizzuality.localhost.lan'},
url: '/tiles/gadm4/6/31/24.png?' + sql,
method: 'GET'
},{
status: 500,
}, function() { done(); });
});
});

View File

@ -28,6 +28,7 @@ psql "${TEST_DB}" < ./sql/gadm4.sql
echo "preparing redis..."
echo "HSET rails:users:vizzuality id 1" | redis-cli -p ${REDIS_PORT} -n 5
echo 'HSET rails:users:vizzuality database_name "'"${TEST_DB}"'"' | redis-cli -p ${REDIS_PORT} -n 5
echo "SADD rails:users:vizzuality:map_key 1234" | 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 "Finished preparing data. Run tests with expresso."

View File

@ -23,7 +23,7 @@ CREATE TABLE gadm4 (
);
GRANT ALL ON TABLE gadm4 TO postgres;
GRANT ALL ON TABLE gadm4 TO tileuser;
GRANT ALL ON TABLE gadm4 TO publicuser;
CREATE SEQUENCE gadm4_seq
START WITH 1
INCREMENT BY 1
@ -100,5 +100,5 @@ CREATE INDEX bdll25_provincias_4326_2_the_geom_webmercator_idx ON gadm4 USING gi
-- development_cartodb_user_3 role
CREATE USER development_cartodb_user_3;
GRANT ALL ON TABLE gadm4 TO development_cartodb_user_3;
GRANT SELECT ON TABLE gadm4 TO tileuser;
GRANT SELECT ON TABLE gadm4 TO publicuser;

View File

@ -15,8 +15,11 @@ SET search_path = public, pg_catalog;
SET default_tablespace = '';
SET default_with_oids = false;
-- tileuser role
CREATE USER tileuser;
-- publicuser role
CREATE USER publicuser;
-- db owner role
CREATE USER test_cartodb_user_1;
-- first table
CREATE TABLE test_table (
@ -59,8 +62,8 @@ ALTER TABLE ONLY test_table ADD CONSTRAINT test_table_pkey PRIMARY KEY (cartodb_
CREATE INDEX test_table_the_geom_idx ON test_table USING gist (the_geom);
CREATE INDEX test_table_the_geom_webmercator_idx ON test_table USING gist (the_geom_webmercator);
GRANT ALL ON TABLE test_table TO postgres;
GRANT ALL ON TABLE test_table TO tileuser;
GRANT ALL ON TABLE test_table TO test_cartodb_user_1;
GRANT SELECT ON TABLE test_table TO publicuser;
-- second table
CREATE TABLE test_table_2 (
@ -103,7 +106,8 @@ ALTER TABLE ONLY test_table_2 ADD CONSTRAINT test_table_2_pkey PRIMARY KEY (cart
CREATE INDEX test_table_2_the_geom_idx ON test_table_2 USING gist (the_geom);
CREATE INDEX test_table_2_the_geom_webmercator_idx ON test_table_2 USING gist (the_geom_webmercator);
GRANT ALL ON TABLE test_table_2 TO tileuser;
GRANT ALL ON TABLE test_table_2 TO test_cartodb_user_1;
GRANT SELECT ON TABLE test_table_2 TO publicuser;
-- third table
CREATE TABLE test_table_3 (
@ -146,5 +150,24 @@ ALTER TABLE ONLY test_table_3 ADD CONSTRAINT test_table_3_pkey PRIMARY KEY (cart
CREATE INDEX test_table_3_the_geom_idx ON test_table_3 USING gist (the_geom);
CREATE INDEX test_table_3_the_geom_webmercator_idx ON test_table_3 USING gist (the_geom_webmercator);
GRANT ALL ON TABLE test_table_3 TO postgres;
GRANT ALL ON TABLE test_table_3 TO tileuser;
GRANT ALL ON TABLE test_table_3 TO test_cartodb_user_1;
GRANT SELECT ON TABLE test_table_3 TO publicuser;
-- private table
CREATE TABLE test_table_private_1 (
updated_at timestamp without time zone DEFAULT now(),
created_at timestamp without time zone DEFAULT now(),
cartodb_id integer NOT NULL,
name character varying,
address character varying,
the_geom geometry,
the_geom_webmercator geometry,
CONSTRAINT enforce_dims_the_geom CHECK ((st_ndims(the_geom) = 2)),
CONSTRAINT enforce_dims_the_geom_webmercator CHECK ((st_ndims(the_geom_webmercator) = 2)),
CONSTRAINT enforce_geotype_the_geom CHECK (((geometrytype(the_geom) = 'POINT'::text) OR (the_geom IS NULL))),
CONSTRAINT enforce_geotype_the_geom_webmercator CHECK (((geometrytype(the_geom_webmercator) = 'POINT'::text) OR (the_geom_webmercator IS NULL))),
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
);
GRANT ALL ON TABLE test_table_private_1 TO test_cartodb_user_1;

View File

@ -8,8 +8,8 @@
var _ = require('underscore');
// set environment specific variables
global.settings = require(__dirname + '/../config/settings');
global.environment = require(__dirname + '/../config/environments/test');
global.settings = require(__dirname + '/../../config/settings');
global.environment = require(__dirname + '/../../config/environments/test');
_.extend(global.settings, global.environment);

View File

@ -0,0 +1,66 @@
var assert = require('assert')
, _ = require('underscore')
, redis = require('redis')
, test_helper = require('../../support/test_helper')
, tests = module.exports = {};
suite('req2params', function() {
// configure redis pool instance to use in tests
var opts = require('../../../lib/cartodb/server_options');
test('can be found in server_options', function(){
assert.ok(_.isFunction(opts.req2params));
});
test('cleans up request', function(done){
opts.req2params({headers: { host:'h1' }, query: {dbuser:'hacker',dbname:'secret'}}, function(err, req) {
if ( err ) { console.log(err); throw new Error(err); }
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
assert.ok(_.isNull(req.params.dbname), 'could forge dbname');
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser');
done();
});
});
test('sets dbname from redis metadata', function(done){
opts.req2params({headers: { host:'vizzuality' }, query: {} }, function(err, req) {
if ( err ) { console.log(err); throw new Error(err); }
//console.dir(req);
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
// database_name for user "vizzuality" (see test/support/prepare_db.sh)
assert.equal(req.params.dbname, 'cartodb_test_user_1_db');
// unauthenticated request gets no dbuser
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser');
done();
});
});
test('sets also dbuser for authenticated requests', function(done){
opts.req2params({headers: { host:'vizzuality' }, query: {map_key: '1234'} }, function(err, req) {
if ( err ) { console.log(err); throw new Error(err); }
//console.dir(req);
assert.ok(_.isObject(req.query), 'request has query');
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
assert.ok(req.hasOwnProperty('params'), 'request has params');
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
// database_name for user "vizzuality" (see test/support/prepare_db.sh)
assert.equal(req.params.dbname, 'cartodb_test_user_1_db');
// id for user "vizzuality" (see test/support/prepare_db.sh)
assert.equal(req.params.dbuser, 'test_cartodb_user_1');
opts.req2params({headers: { host:'vizzuality' }, query: {map_key: '1235'} }, function(err, req) {
// wrong key resets params to no user
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser');
done();
});
});
});
});