diff --git a/app/controllers/app.js b/app/controllers/app.js index 136caa89..3488d99d 100755 --- a/app/controllers/app.js +++ b/app/controllers/app.js @@ -19,10 +19,11 @@ var express= require('express') buffer: true, format: '[:date] :req[X-Real-IP] \033[90m:method\033[0m \033[36m:req[Host]:url\033[0m \033[90m:status :response-time ms -> :res[Content-Type]\033[0m' })) - , Step = require('step') - , Meta = require(global.settings.app_root + '/app/models/metadata') - , oAuth = require(global.settings.app_root + '/app/models/oauth') - , PSQL = require(global.settings.app_root + '/app/models/psql') + , Step = require('step') + , Meta = require(global.settings.app_root + '/app/models/metadata') + , oAuth = require(global.settings.app_root + '/app/models/oauth') + , PSQL = require(global.settings.app_root + '/app/models/psql') + , ApiKeyAuth = require(global.settings.app_root + '/app/models/apikey_auth') , _ = require('underscore'); app.use(express.bodyParser()); @@ -35,6 +36,7 @@ function handleQuery(req, res){ // sanitize input var body = (req.body) ? req.body : {}; var sql = req.query.q || body.q; // get and post + var api_key = req.query.api_key || body.api_key; var database = req.query.database; // deprecate this in future var limit = parseInt(req.query.rows_per_page); var offset = parseInt(req.query.page); @@ -68,7 +70,11 @@ function handleQuery(req, res){ function setDBGetUser(err, data) { if (err) throw err; database = (data == "" || _.isNull(data)) ? database : data; - oAuth.verifyRequest(req, this); + if(api_key) { + ApiKeyAuth.verifyRequest(req, this); + } else { + oAuth.verifyRequest(req, this); + } }, function querySql(err, user_id){ if (err) throw err; diff --git a/app/models/apikey_auth.js b/app/models/apikey_auth.js new file mode 100644 index 00000000..92c3210e --- /dev/null +++ b/app/models/apikey_auth.js @@ -0,0 +1,114 @@ +/** + * this module allows to auth user using an pregenerated api key + */ + +var RedisPool = require("./redis_pool") + , _ = require('underscore') + , Step = require('step'); + +module.exports = (function() { + + var me = { + user_metadata_db: 5, + table_metadata_db: 0, + user_key: "rails:users:<%= username %>", + map_key: "rails:users:<%= username %>:map_key", + table_key: "rails:<%= database_name %>:<%= table_name %>" + }; + + me.retrieve = function(db, redisKey, hashKey, callback) { + this.redisCmd(db,'HGET',[redisKey, hashKey], callback); + }; + + me.inSet = function(db, setKey, member, callback) { + this.redisCmd(db,'SISMEMBER',[setKey, member], callback); + }; + + /** + * Use Redis + * + * @param db - redis database number + * @param redisFunc - the redis function to execute + * @param redisArgs - the arguments for the redis function in an array + * @param callback - function to pass results too. + */ + me.redisCmd = function(db, redisFunc, redisArgs, callback) { + + var redisClient; + Step( + function() { + var step = this; + RedisPool.acquire(db, function(_redisClient) { + redisClient = _redisClient; + redisArgs.push(step); + redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs); + }); + }, + function releaseRedisClient(err, data) { + if (err) throw err; + RedisPool.release(db, redisClient); + callback(err, data); + } + ); + }; + + + /** + * Get the user id for this particular subdomain/username + * + * @param req - standard express req object. importantly contains host information + * @param callback + */ + me.getId = function(req, callback) { + // strip subdomain from header host + var username = req.headers.host.split('.')[0]; + var redisKey = _.template(this.user_key, {username: username}); + + this.retrieve(this.user_metadata_db, redisKey, 'id', callback); + }; + + /** + * Get the user map key for this particular subdomain/username + * + * @param req - standard express req object. importantly contains host information + * @param callback + */ + me.checkAPIKey= function(req, callback) { + // strip subdomain from header host + var username = req.headers.host.split('.')[0]; + var redisKey = _.template(this.map_key, {username: username}); + var api_key = req.query.api_key || req.body.api_key; + this.inSet(this.user_metadata_db, redisKey, api_key, callback); + }; + + /** + * Get privacy for cartodb table + * + * @param req - standard req object. Importantly contains table and host information + * @param callback - user_id if ok, null if auth fails + */ + me.verifyRequest = function(req, callback) { + var that = this; + + Step( + // check api key + function(){ + that.checkAPIKey(req, this); + }, + // get user id or fail + function (err, apikey_valid) { + if (apikey_valid) { + that.getId(req, this); + } else { + // no auth + callback(false, null); + } + }, + function (err, user_id){ + if (err) throw err; + callback(false, user_id); + } + ); + }; + return me; +})(); diff --git a/app/models/oauth.js b/app/models/oauth.js index d5ba7e45..7e862005 100644 --- a/app/models/oauth.js +++ b/app/models/oauth.js @@ -65,7 +65,7 @@ var oAuth = function(){ me.verifyRequest = function(req, callback){ var that = this; //TODO: review this - var http = arguments['2']; + var http = true;//arguments['2']; var passed_tokens; var ohash; var signature; diff --git a/test/acceptance/app.auth.test.js b/test/acceptance/app.auth.test.js new file mode 100644 index 00000000..69516e82 --- /dev/null +++ b/test/acceptance/app.auth.test.js @@ -0,0 +1,32 @@ +require('../helper'); + +var app = require(global.settings.app_root + '/app/controllers/app') + , assert = require('assert') + , tests = module.exports = {} + , querystring = require('querystring'); + +tests['valid api key should allow insert in protected tables'] = function(){ + assert.response(app, { + // view prepare_db.sh to see where to set api_key + url: "/api/v1/sql?api_key=1234&q=INSERT%20INTO%20private_table%20(name)%20VALUES%20('test')&database=cartodb_dev_user_1_db", + + headers: {host: 'vizzuality.cartodb.com' }, + method: 'GET' + },{ + status: 200 + }); +} + +tests['invalid api key should NOT allow insert in protected tables'] = function(){ + assert.response(app, { + // view prepare_db.sh to see where to set api_key + url: "/api/v1/sql?api_key=RAMBO&q=INSERT%20INTO%20private_table%20(name)%20VALUES%20('test')&database=cartodb_dev_user_1_db", + + headers: {host: 'vizzuality.cartodb.com' }, + method: 'GET' + },{ + status: 400 + }); +} + + diff --git a/test/acceptance/app.test.js b/test/acceptance/app.test.js index 15b255fb..64a4bc1d 100644 --- a/test/acceptance/app.test.js +++ b/test/acceptance/app.test.js @@ -24,7 +24,6 @@ app.setMaxListeners(0); var real_oauth_header = 'OAuth realm="http://vizzuality.testhost.lan/",oauth_consumer_key="fZeNGv5iYayvItgDYHUbot1Ukb5rVyX6QAg8GaY2",oauth_token="l0lPbtP68ao8NfStCiA3V3neqfM03JKhToxhUQTR",oauth_signature_method="HMAC-SHA1", oauth_signature="o4hx4hWP6KtLyFwggnYB4yPK8xI%3D",oauth_timestamp="1313581372",oauth_nonce="W0zUmvyC4eVL8cBd4YwlH1nnPTbxW0QBYcWkXTwe4",oauth_version="1.0"'; - tests['GET /api/v1/sql'] = function(){ assert.response(app, { url: '/api/v1/sql', @@ -163,4 +162,4 @@ function checkDecimals(x, dec_sep){ return tmp.length-tmp.indexOf(dec_sep)-1; else return 0; -} \ No newline at end of file +} diff --git a/test/prepare_db.sh b/test/prepare_db.sh index 3df20758..33618d03 100755 --- a/test/prepare_db.sh +++ b/test/prepare_db.sh @@ -5,6 +5,7 @@ echo "preparing redis..." echo "HSET rails:users:vizzuality id 1" | redis-cli -n 5 echo "HSET rails:users:vizzuality database_name cartodb_test_user_1_db" | redis-cli -n 5 +echo "SADD rails:users:vizzuality:map_key 1234" | redis-cli -n 5 echo "preparing postgres..." dropdb -Upostgres -hlocalhost cartodb_test_user_1_db diff --git a/test/test.sql b/test/test.sql index c1cfd447..5b7cb8d5 100644 --- a/test/test.sql +++ b/test/test.sql @@ -62,8 +62,51 @@ ALTER TABLE ONLY untitle_table_4 ADD CONSTRAINT test_table_pkey PRIMARY KEY (car CREATE INDEX test_table_the_geom_idx ON untitle_table_4 USING gist (the_geom); CREATE INDEX test_table_the_geom_webmercator_idx ON untitle_table_4 USING gist (the_geom_webmercator); +CREATE TABLE private_table ( + 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)) +); + +CREATE SEQUENCE test_table_cartodb_id_seq_p + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE test_table_cartodb_id_seq_p OWNED BY private_table.cartodb_id; + +SELECT pg_catalog.setval('test_table_cartodb_id_seq_p', 60, true); + +ALTER TABLE private_table ALTER COLUMN cartodb_id SET DEFAULT nextval('test_table_cartodb_id_seq_p'::regclass); + +INSERT INTO private_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241'); +INSERT INTO private_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241'); +INSERT INTO private_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241'); +INSERT INTO private_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241'); +INSERT INTO private_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241'); + +ALTER TABLE ONLY private_table ADD CONSTRAINT test_table_pkey_p PRIMARY KEY (cartodb_id); + +CREATE INDEX test_table_the_geom_idx_p ON private_table USING gist (the_geom); +CREATE INDEX test_table_the_geom_webmercator_idx_p ON private_table USING gist (the_geom_webmercator); + CREATE USER publicuser WITH PASSWORD ''; +CREATE USER test_cartodb_user_1 WITH PASSWORD ''; GRANT SELECT ON TABLE untitle_table_4 TO publicuser; +GRANT ALL ON TABLE private_table TO test_cartodb_user_1; +GRANT ALL ON SEQUENCE test_table_cartodb_id_seq_p TO test_cartodb_user_1