New authentication mechanism: checks in advance if credentials are provided

in order to do a single request to redis to retrieve the required database
connection parameters.
This commit is contained in:
Raul Ochoa 2014-08-05 16:20:06 +02:00
parent 49406c99fa
commit 480a9f27b4
10 changed files with 211 additions and 23 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ node_modules*
tools/munin/cdbsqlapi.conf
test/redis.pid
test/test.log
test/acceptance/oauth/venv/*

View File

@ -1,6 +1,13 @@
1.12.2 - 2014-mm-dd
1.13.0 - 2014-mm-dd
-------------------
New features:
* New authentication mechanism: checks in advance if credentials are provided
in order to do a single request to redis to retrieve the required database
connection parameters.
1.12.1 - 2014-08-05
-------------------

View File

@ -1,11 +1,23 @@
/**
* this module allows to auth user using an pregenerated api key
*/
function ApikeyAuth() {
function ApikeyAuth(req) {
this.req = req;
}
module.exports = ApikeyAuth;
ApikeyAuth.prototype.verifyCredentials = function(options, callback) {
verifyRequest(this.req, options.apiKey, callback);
};
ApikeyAuth.prototype.hasCredentials = function() {
return !!(this.req.query.api_key
|| this.req.query.map_key
|| (this.req.body && this.req.body.api_key)
|| (this.req.body && this.req.body.map_key));
};
/**
* Get id of authorized user
*
@ -13,7 +25,7 @@ module.exports = ApikeyAuth;
* @param {String} requiredApi - the API associated to the user, req must contain it
* @param {Function} callback - err, boolean (whether the request is authenticated or not)
*/
ApikeyAuth.prototype.verifyRequest = function (req, requiredApi, callback) {
function verifyRequest(req, requiredApi, callback) {
var valid = false;
@ -31,4 +43,4 @@ ApikeyAuth.prototype.verifyRequest = function (req, requiredApi, callback) {
}
callback(null, valid);
};
}

34
app/auth/auth_api.js Normal file
View File

@ -0,0 +1,34 @@
var ApiKeyAuth = require('./apikey'),
OAuthAuth = require('./oauth');
function AuthApi(req, requestParams) {
this.req = req;
this.authBacked = getAuthBackend(req, requestParams);
this._hasCredentials = null;
}
AuthApi.prototype.hasCredentials = function() {
if (this._hasCredentials === null) {
this._hasCredentials = this.authBacked.hasCredentials();
}
return this._hasCredentials;
};
AuthApi.prototype.verifyCredentials = function(options, callback) {
if (this.hasCredentials()) {
this.authBacked.verifyCredentials(options, callback);
} else {
callback(null, false);
}
};
function getAuthBackend(req, requestParams) {
if (requestParams.api_key) {
return new ApiKeyAuth(req);
} else {
return new OAuthAuth(req);
}
}
module.exports = AuthApi;

View File

@ -157,4 +157,28 @@ var oAuth = function(){
return me;
}();
module.exports = oAuth;
function OAuthAuth(req) {
this.req = req;
this.isOAuthRequest = null;
}
OAuthAuth.prototype.verifyCredentials = function(options, callback) {
if (this.hasCredentials()) {
oAuth.verifyRequest(this.req, callback, options.requestProtocol);
} else {
callback(null, false);
}
};
OAuthAuth.prototype.hasCredentials = function() {
if (this.isOAuthRequest === null) {
var passed_tokens = oAuth.parseTokens(this.req);
this.isOAuthRequest = !_.isEmpty(passed_tokens);
}
return this.isOAuthRequest;
};
module.exports = OAuthAuth;
module.exports.backend = oAuth;

View File

@ -36,8 +36,7 @@ var express = require('express')
, PSQL = require(global.settings.app_root + '/app/models/psql')
, PSQLWrapper = require(global.settings.app_root + '/app/sql/psql_wrapper')
, CdbRequest = require(global.settings.app_root + '/app/models/cartodb_request')
, oAuth = require(global.settings.app_root + '/app/auth/oauth')
, ApiKeyAuth = require(global.settings.app_root + '/app/auth/apikey')
, AuthApi = require(global.settings.app_root + '/app/auth/auth_api')
, _ = require('underscore')
, LRU = require('lru-cache')
, formats = require(global.settings.app_root + '/app/models/formats')
@ -51,7 +50,6 @@ var metadataBackend = MetadataDB({
reapIntervalMillis: global.settings.redisReapIntervalMillis
});
var cdbReq = new CdbRequest();
var apiKeyAuth = new ApiKeyAuth(metadataBackend, cdbReq);
// Set default configuration
global.settings.db_pubuser = global.settings.db_pubuser || "publicuser";
@ -283,11 +281,12 @@ function handleQuery(req, res) {
var formatter;
var cdbuser = cdbReq.userByReq(req);
var cdbUsername = cdbReq.userByReq(req),
authApi = new AuthApi(req, params),
dbParams;
if ( req.profiler ) req.profiler.done('init');
var dbParams;
// 1. Get database from redis via the username stored in the host header subdomain
// 2. Run the request through OAuth to get R/W user id if signed
// 3. Get the list of tables affected by the query
@ -296,12 +295,18 @@ function handleQuery(req, res) {
Step(
function getDatabaseConnectionParams() {
checkAborted('getDatabaseConnectionParams');
metadataBackend.getAllUserDBParams(cdbuser, this);
// If the request is providing credentials it may require every DB parameters
if (authApi.hasCredentials()) {
metadataBackend.getAllUserDBParams(cdbUsername, this);
} else {
metadataBackend.getUserDBPublicConnectionParams(cdbUsername, this);
}
},
function authenticate(err, userDBParams) {
console.log(err);
if (err) {
err.http_status = 404;
err.message = "Sorry, we can't find CartoDB user '" + cdbuser
err.message = "Sorry, we can't find CartoDB user '" + cdbUsername
+ "'. Please check that you have entered the correct domain.";
throw err;
}
@ -312,11 +317,7 @@ function handleQuery(req, res) {
dbopts.dbname = dbParams.dbname;
dbopts.user = (!!dbParams.dbpublicuser) ? dbParams.dbpublicuser : global.settings.db_pubuser;
if (api_key) {
apiKeyAuth.verifyRequest(req, dbParams.apikey, this);
} else {
oAuth.verifyRequest(req, this, requestProtocol);
}
authApi.verifyCredentials({apiKey: dbParams.apikey, requestProtocol: requestProtocol}, this);
},
function setDBAuth(err, isAuthenticated) {
if (err) {

6
npm-shrinkwrap.json generated
View File

@ -1,6 +1,6 @@
{
"name": "cartodb_sql_api",
"version": "1.12.2",
"version": "1.13.0",
"dependencies": {
"underscore": {
"version": "1.3.3"
@ -48,8 +48,8 @@
}
},
"cartodb-redis": {
"version": "0.6.0",
"from": "git://github.com/CartoDB/node-cartodb-redis.git#0.6.0",
"version": "0.7.0",
"from": "git://github.com/CartoDB/node-cartodb-redis.git#0.7.0",
"dependencies": {
"strftime": {
"version": "0.6.2"

View File

@ -5,7 +5,7 @@
"keywords": [
"cartodb"
],
"version": "1.12.2",
"version": "1.13.0",
"repository": {
"type": "git",
"url": "git://github.com/CartoDB/CartoDB-SQL-API.git"
@ -20,7 +20,7 @@
"underscore.string": "~1.1.6",
"pg": "git://github.com/CartoDB/node-postgres.git#2.6.2-cdb1",
"express": "~2.5.11",
"cartodb-redis": "~0.6.0",
"cartodb-redis": "git://github.com/CartoDB/node-cartodb-redis.git#0.7.0",
"step": "0.0.x",
"topojson": "0.0.8",
"oauth-client": "0.2.0",

View File

@ -0,0 +1,93 @@
require('../helper');
var _ = require('underscore')
, ApikeyAuth = require('../../app/auth/apikey')
, assert = require('assert')
;
suite('has credentials', function() {
var noCredentialsRequests = [
{
des: 'there is not api_key/map_key in the request query',
req: {query:{}}
},
{
des: 'api_key is undefined`ish in the request query',
req: {query:{api_key:null}}
},
{
des: 'map_key is undefined`ish in the request query',
req: {query:{map_key:null}}
},
{
des: 'there is not api_key/map_key in the request body',
req: {query:{}, body:{}}
},
{
des: 'api_key is undefined`ish in the request body',
req: {query:{}, body:{api_key:null}}
},
{
des: 'map_key is undefined`ish in the request body',
req: {query:{}, body:{map_key:null}}
}
];
noCredentialsRequests.forEach(function(request) {
test('has no credentials if ' + request.des, function() {
testCredentials(request.req, false)
});
});
var credentialsRequests = [
{
des: 'there is api_key in the request query',
req: {query:{api_key: 'foo'}}
},
{
des: 'there is api_key in the request query',
req: {query:{map_key: 'foo'}}
},
{
des: 'there is api_key in the request body',
req: {query:{}, body:{api_key:'foo'}}
},
{
des: 'there is map_key in the request body',
req: {query:{}, body:{map_key:'foo'}}
}
];
credentialsRequests.forEach(function(request) {
test('has credentials if ' + request.des, function() {
testCredentials(request.req, true)
});
});
function testCredentials(req, hasCredentials) {
var apiKeyAuth = new ApikeyAuth(req);
assert.equal(apiKeyAuth.hasCredentials(), hasCredentials);
}
});
suite('verify credentials', function() {
test('verifyCredentials callbacks with true value when request api_key is the same', function(done) {
testVerifyCredentials({query:{api_key: 'foo'}}, {apiKey: 'foo'}, true, done);
});
test('verifyCredentials callbacks with true value when request api_key is different', function(done) {
testVerifyCredentials({query:{api_key: 'foo'}}, {apiKey: 'bar'}, false, done);
});
function testVerifyCredentials(req, options, shouldBeValid, done) {
var apiKeyAuth = new ApikeyAuth(req);
apiKeyAuth.verifyCredentials(options, function(err, validCredentials) {
assert.equal(validCredentials, shouldBeValid);
done();
});
}
});

View File

@ -2,7 +2,8 @@ require('../helper');
var _ = require('underscore')
, redis = require("redis")
, oAuth = require('../../app/auth/oauth')
, OAuthAuth = require('../../app/auth/oauth')
, oAuth = require('../../app/auth/oauth').backend
, assert = require('assert')
, tests = module.exports = {}
, oauth_data_1 = {
@ -107,4 +108,19 @@ test('returns null user for no oauth', function(done){
});
});
test('OAuthAuth reports it has credentials', function(done) {
var req = {query:{}, headers:{authorization:real_oauth_header}};
var oAuthAuth = new OAuthAuth(req);
assert.ok(oAuthAuth.hasCredentials());
done();
});
test('OAuthAuth reports it has no credentials', function(done) {
var req = {query:{}, headers:{}};
var oAuthAuth = new OAuthAuth(req);
assert.equal(oAuthAuth.hasCredentials(), false);
done();
});
});