Skip to content

Commit 5eef473

Browse files
author
Sandro Santilli
committed
Generalize CartoDB username extraction, allowing for multiuser setups
Closes #100
1 parent 46bc0eb commit 5eef473

11 files changed

+163
-51
lines changed

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
1.8.0 - 2013-MM-DD
22
------------------
33

4+
* Add 'user_from_host' directive to generalize username extraction (#124)
5+
46
1.7.1 - 2013-12-02
57
------------------
68

app/controllers/app.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,16 @@ var express = require('express')
3636
// global.settings.app_root + '/app/models/metadata')
3737
, oAuth = require(global.settings.app_root + '/app/models/oauth')
3838
, PSQL = require(global.settings.app_root + '/app/models/psql')
39+
, CdbRequest = require(global.settings.app_root + '/app/models/cartodb_request')
3940
, ApiKeyAuth = require(global.settings.app_root + '/app/models/apikey_auth')
4041
, _ = require('underscore')
4142
, LRU = require('lru-cache')
4243
, formats = require(global.settings.app_root + '/app/models/formats')
4344
;
4445

46+
var cdbReq = new CdbRequest(Meta);
47+
var apiKeyAuth = new ApiKeyAuth(Meta, cdbReq);
48+
4549
// Set default configuration
4650
global.settings.db_pubuser = global.settings.db_pubuser || "publicuser";
4751

@@ -173,6 +177,8 @@ function handleQuery(req, res) {
173177

174178
var formatter;
175179

180+
var cdbuser = cdbReq.userByReq(req);
181+
176182
// 1. Get database from redis via the username stored in the host header subdomain
177183
// 2. Run the request through OAuth to get R/W user id if signed
178184
// 3. Get the list of tables affected by the query
@@ -181,7 +187,7 @@ function handleQuery(req, res) {
181187
Step(
182188
function getDatabaseName() {
183189
if (_.isNull(database)) {
184-
Meta.getDatabase(req, this);
190+
Meta.getUserDBName(cdbuser, this);
185191
} else {
186192
// database hardcoded in query string (deprecated??): don't use redis
187193
return database;
@@ -201,7 +207,7 @@ function handleQuery(req, res) {
201207
dbopts.dbname = database;
202208

203209
if(api_key) {
204-
ApiKeyAuth.verifyRequest(req, this);
210+
apiKeyAuth.verifyRequest(req, this);
205211
} else {
206212
oAuth.verifyRequest(req, this, requestProtocol);
207213
}
@@ -218,7 +224,7 @@ function handleQuery(req, res) {
218224

219225
dbopts.user = dbuser;
220226

221-
Meta.getDatabaseHost(req, this);
227+
Meta.getUserDBHost(cdbuser, this);
222228
},
223229
function setDBHostGetPassword(err, data){
224230
if (err) throw err;
@@ -228,7 +234,7 @@ function handleQuery(req, res) {
228234
// by-pass redis lookup for password if not authenticated
229235
if ( ! authenticated ) return null;
230236

231-
Meta.getDatabasePassword(req, this);
237+
Meta.getUserDBPass(cdbuser, this);
232238
},
233239
function queryExplain(err, data){
234240
if (err) throw err;

app/models/apikey_auth.js

Lines changed: 82 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,86 @@
22
* this module allows to auth user using an pregenerated api key
33
*/
44

5-
var Meta = require("cartodb-redis")({
6-
host: global.settings.redis_host,
7-
port: global.settings.redis_port
8-
})
9-
, _ = require('underscore')
10-
, Step = require('step');
11-
12-
module.exports = (function() {
13-
14-
var me = {}
15-
16-
/**
17-
* Get privacy for cartodb table
18-
*
19-
* @param req - standard req object. Importantly contains table and host information
20-
* @param callback - err, user_id (null if no auth)
21-
*/
22-
me.verifyRequest = function(req, callback) {
23-
var that = this;
24-
25-
Step(
26-
// check api key
27-
function(){
28-
Meta.checkAPIKey(req, this);
29-
},
30-
// get user id or fail
31-
function (err, apikey_valid) {
32-
if ( err ) throw err;
33-
if (apikey_valid) {
34-
Meta.getId(req, this);
35-
} else {
36-
// no auth
37-
callback(null, null);
38-
}
39-
},
40-
function (err, user_id){
41-
callback(err, user_id);
5+
var _ = require('underscore')
6+
, Step = require('step');
7+
8+
function ApikeyAuth(cartodb_redis, cartodb_request) {
9+
if ( ! cartodb_redis ) throw new Error("Cannot initialize ApikeyAuth with no cartodb_request");
10+
if ( ! cartodb_request ) throw new Error("Cannot initialize ApikeyAuth with no cartodb-redis");
11+
this.cdbRedis = cartodb_redis;
12+
this.cdbRequest = cartodb_request;
13+
}
14+
15+
module.exports = ApikeyAuth;
16+
17+
var o = ApikeyAuth.prototype;
18+
19+
o.userByReq = function(req) {
20+
return this.cdbRequest.userByReq(req)
21+
};
22+
23+
// Check if a request is authorized by api_key
24+
//
25+
// @param req express request object
26+
// @param callback function(err, authorized)
27+
//
28+
o.authorizedByAPIKey = function(req, callback)
29+
{
30+
var user = this.userByReq(req);
31+
var that = this;
32+
Step(
33+
function (){
34+
that.cdbRedis.getUserMapKey(user, this);
35+
},
36+
function checkApiKey(err, val){
37+
if (err) throw err;
38+
39+
var valid = 0;
40+
if ( val ) {
41+
if ( val == req.query.map_key ) valid = 1;
42+
else if ( val == req.query.api_key ) valid = 1;
43+
// check also in request body
44+
else if ( req.body && req.body.map_key && val == req.body.map_key ) valid = 1;
45+
else if ( req.body && req.body.api_key && val == req.body.api_key ) valid = 1;
46+
}
47+
48+
return valid;
49+
},
50+
function finish(err, authorized) {
51+
callback(err, authorized);
52+
}
53+
);
54+
};
55+
56+
57+
/**
58+
* Get id of authorized user
59+
*
60+
* @param req - standard req object. Importantly contains table and host information
61+
* @param callback - err, user_id (null if no auth)
62+
*/
63+
o.verifyRequest = function(req, callback) {
64+
var user = this.userByReq(req);
65+
var that = this;
66+
67+
Step(
68+
// check api key
69+
function(){
70+
that.authorizedByAPIKey(req, this);
71+
},
72+
// get user id or fail
73+
function (err, apikey_valid) {
74+
if ( err ) throw err;
75+
if (apikey_valid) {
76+
that.cdbRedis.getUserId(user, this);
77+
} else {
78+
// no auth
79+
callback(null, null);
4280
}
43-
);
44-
};
45-
return me;
46-
})();
81+
},
82+
function (err, user_id){
83+
callback(err, user_id);
84+
}
85+
);
86+
};
87+

app/models/cartodb_request.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* this module provides cartodb-specific interpretation
3+
* of request headers
4+
*/
5+
6+
function CartodbRequest(cartodb_redis) {
7+
this.cartodb_redis = cartodb_redis;
8+
}
9+
10+
module.exports = CartodbRequest;
11+
12+
var o = CartodbRequest.prototype;
13+
14+
o.re_userFromHost = new RegExp(
15+
global.settings.user_from_host ||
16+
'^([^\\.]+)\\.' // would extract "strk" from "strk.cartodb.com"
17+
);
18+
19+
o.userByReq = function(req) {
20+
var host = req.headers.host;
21+
var mat = host.match(this.re_userFromHost);
22+
if ( ! mat ) {
23+
console.error("ERROR: user pattern '" + this.re_userFromHost
24+
+ "' does not match hostname '" + host + "'");
25+
return;
26+
}
27+
// console.log("Matches: "); console.dir(mat);
28+
if ( ! mat.length === 2 ) {
29+
console.error("ERROR: pattern '" + this.re_userFromHost
30+
+ "' gave unexpected matches against '" + host + "': " + mat);
31+
return;
32+
}
33+
return mat[1];
34+
};
35+

app/models/oauth.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// too bound to the request object, but ok for now
2-
var RedisPool = require("../../node_modules/cartodb-redis/lib/redis_pool.js")({
2+
var RedisPool = require("../../node_modules/cartodb-redis/node_modules/redis-mpool/")({
33
host: global.settings.redis_host,
44
port: global.settings.redis_port,
55
})

config/environments/development.js.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
module.exports.base_url = '/api/:version';
2+
// Regular expression pattern to extract username
3+
// from hostname. Must have a single grabbing block.
4+
module.exports.user_from_host = '^(.*)\\.localhost';
25
module.exports.node_port = 8080;
36
module.exports.node_host = '127.0.0.1';
47
// idle socket timeout, in miliseconds

config/environments/production.js.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
module.exports.base_url = '/api/:version';
2+
// Regular expression pattern to extract username
3+
// from hostname. Must have a single grabbing block.
4+
module.exports.user_from_host = '^(.*)\\.cartodb\\.com$';
25
module.exports.node_port = 8080;
36
module.exports.node_host = '127.0.0.1';
47
// idle socket timeout, in miliseconds

config/environments/staging.js.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
module.exports.base_url = '/api/:version';
2+
// Regular expression pattern to extract username
3+
// from hostname. Must have a single grabbing block.
4+
module.exports.user_from_host = '^(.*)\\.cartodb\\.com$';
25
module.exports.node_port = 8080;
36
module.exports.node_host = '127.0.0.1';
47
// idle socket timeout, in miliseconds

config/environments/test.js.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
module.exports.base_url = '/api/:version';
2+
// Regular expression pattern to extract username
3+
// from hostname. Must have a single grabbing block.
4+
module.exports.user_from_host = '^([^.]*)\\.';
25
module.exports.node_port = 8080;
36
module.exports.node_host = '127.0.0.1';
47
// idle socket timeout, in miliseconds

npm-shrinkwrap.json

Lines changed: 20 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"underscore.string": "~1.1.6",
2222
"pg": "~2.6.2",
2323
"express": "~2.5.11",
24-
"cartodb-redis": "~0.1.0",
24+
"cartodb-redis": "~0.3.0",
2525
"step": "0.0.x",
2626
"topojson": "0.0.8",
2727
"oauth-client": "0.2.0",

0 commit comments

Comments
 (0)