diff --git a/lib/cartodb/api/auth_api.js b/lib/cartodb/api/auth_api.js index ef874483..8782f907 100644 --- a/lib/cartodb/api/auth_api.js +++ b/lib/cartodb/api/auth_api.js @@ -1,3 +1,5 @@ +var _ = require('underscore'); // AUTH_FALLBACK + /** * * @param {PgConnection} pgConnection @@ -45,10 +47,10 @@ AuthApi.prototype.authorizedBySigner = function(res, callback) { }; function isValidApiKey(apikey) { - return apikey.type !== null && - apikey.user !== null && - apikey.databasePassword !== null && - apikey.databaseRole !== null; + return apikey.type && + apikey.user && + apikey.databasePassword && + apikey.databaseRole; } // Check if a request is authorized by api_key @@ -68,8 +70,18 @@ AuthApi.prototype.authorizedByAPIKey = function(user, res, callback) { this.metadataBackend.getApikey(user, apikeyToken, (err, apikey) => { if (err) { + if (isNameNotFoundError(err)) { + err.http_status = 404; + } + return callback(err); } + + //Remove this block when Auth fallback is not used anymore + // AUTH_FALLBACK + apikey.databaseRole = composeUserDatabase(apikey); + apikey.databasePassword = composeDatabasePassword(apikey); + if ( !isValidApiKey(apikey)) { const error = new Error('Unauthorized'); error.type = 'auth'; @@ -79,13 +91,13 @@ AuthApi.prototype.authorizedByAPIKey = function(user, res, callback) { return callback(error); } - if (apikeyUsername && (apikeyUsername !== res.locals.user)) { + if (!usernameMatches(apikeyUsername, res.locals.user)) { const error = new Error('Forbidden'); error.type = 'auth'; error.subtype = 'api-key-username-mismatch'; error.http_status = 403; - return callback(error); + return callback(error); } if (!apikey.grantsMaps) { @@ -101,6 +113,46 @@ AuthApi.prototype.authorizedByAPIKey = function(user, res, callback) { }); }; +//Remove this block when Auth fallback is not used anymore +// AUTH_FALLBACK +function composeUserDatabase (apikey) { + if (shouldComposeUserDatabase(apikey)) { + return _.template(global.environment.postgres_auth_user, apikey); + } + + return apikey.databaseRole; +} + +//Remove this block when Auth fallback is not used anymore +// AUTH_FALLBACK +function composeDatabasePassword (apikey) { + if (shouldComposeDatabasePassword(apikey)) { + return global.environment.postgres.password; + } + + return apikey.databasePassword; +} + +//Remove this block when Auth fallback is not used anymore +// AUTH_FALLBACK +function shouldComposeDatabasePassword (apikey) { + return !apikey.databasePassword && global.environment.postgres.password; +} + +//Remove this block when Auth fallback is not used anymore +// AUTH_FALLBACK +function shouldComposeUserDatabase(apikey) { + return !apikey.databaseRole && apikey.user_id && global.environment.postgres_auth_user; +} + +function isNameNotFoundError (err) { + return err.message && -1 !== err.message.indexOf('name not found'); +} + +function usernameMatches (apikeyUsername, requestUsername) { + return !(apikeyUsername && (apikeyUsername !== requestUsername)); +} + /** * Check access authorization * @@ -122,8 +174,8 @@ AuthApi.prototype.authorize = function(req, res, callback) { if (err) { return callback(err); - } - + } + callback(null, true); }); } @@ -132,17 +184,17 @@ AuthApi.prototype.authorize = function(req, res, callback) { if (err) { return callback(err); } - + if (isAuthorizedBySigner) { return this.pgConnection.setDBAuth(user, res.locals, 'master', function (err) { req.profiler.done('setDBAuth'); - + if (err) { return callback(err); - } + } callback(null, true); - }); + }); } // if no signer name was given, use default api key @@ -155,7 +207,7 @@ AuthApi.prototype.authorize = function(req, res, callback) { } callback(null, true); - }); + }); } // if signer name was given, return no authorization diff --git a/lib/cartodb/backends/pg_connection.js b/lib/cartodb/backends/pg_connection.js index b7d81721..290fb6ef 100644 --- a/lib/cartodb/backends/pg_connection.js +++ b/lib/cartodb/backends/pg_connection.js @@ -22,34 +22,74 @@ PgConnection.prototype.setDBAuth = function(username, params, apikeyType, callba if (apikeyType === 'master') { this.metadataBackend.getMasterApikey(username, (err, apikey) => { if (err) { + if (isNameNotFoundError(err)) { + err.http_status = 404; + } return callback(err); } params.dbuser = apikey.databaseRole; params.dbpassword = apikey.databasePassword; + //Remove this block when Auth fallback is not used anymore + // AUTH_FALLBACK + if (!params.dbuser && apikey.user_id && global.environment.postgres_auth_user) { + params.dbuser = _.template(global.environment.postgres_auth_user, apikey); + } + return callback(); }); - } else if (apikeyType === 'regular') { + } else if (apikeyType === 'regular') { //Actually it can be any type of api key this.metadataBackend.getApikey(username, params.api_key, (err, apikey) => { if (err) { + if (isNameNotFoundError(err)) { + err.http_status = 404; + } return callback(err); } params.dbuser = apikey.databaseRole; params.dbpassword = apikey.databasePassword; - return callback(); + //Remove this block when Auth fallback is not used anymore + // AUTH_FALLBACK + if (!params.dbuser && apikey.user_id && apikey.type === 'master' && global.environment.postgres_auth_user) { + params.dbuser = _.template(global.environment.postgres_auth_user, apikey); + } + + //Remove this block when Auth fallback is not used anymore + // AUTH_FALLBACK + if (!params.dbpassword && global.environment.postgres.password) { + params.dbpassword = global.environment.postgres.password; + } + + //Remove this block when Auth fallback is not used anymore + // AUTH_FALLBACK + // If api key not found use default + if (!params.dbuser && !params.dbpassword) { + return this.setDBAuth(username, params, 'default', callback); + } + + return callback(); }); } else if (apikeyType === 'default') { this.metadataBackend.getApikey(username, 'default_public', (err, apikey) => { if (err) { + if (isNameNotFoundError(err)) { + err.http_status = 404; + } return callback(err); } params.dbuser = apikey.databaseRole; params.dbpassword = apikey.databasePassword; + //Remove this block when Auth fallback is not used anymore + // AUTH_FALLBACK + if (!params.dbpassword && global.environment.postgres.password) { + params.dbpassword = global.environment.postgres.password; + } + return callback(); }); } else { @@ -57,6 +97,11 @@ PgConnection.prototype.setDBAuth = function(username, params, apikeyType, callba } }; +function isNameNotFoundError (err) { + return err.message && -1 !== err.message.indexOf('name not found'); +} + + // Set db connection parameters to those for the given username // // @param dbowner cartodb username of database owner, diff --git a/test/acceptance/auth/authorization-fallback.js b/test/acceptance/auth/authorization-fallback.js new file mode 100644 index 00000000..8546c8ff --- /dev/null +++ b/test/acceptance/auth/authorization-fallback.js @@ -0,0 +1,181 @@ +//Remove this file when Auth fallback is not used anymore +// AUTH_FALLBACK + +const assert = require('../../support/assert'); +const testHelper = require('../../support/test_helper'); +const CartodbWindshaft = require('../../../lib/cartodb/server'); +const serverOptions = require('../../../lib/cartodb/server_options'); +const server = new CartodbWindshaft(serverOptions); +var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token'); + +function singleLayergroupConfig(sql, cartocss) { + return { + version: '1.7.0', + layers: [ + { + type: 'mapnik', + options: { + sql: sql, + cartocss: cartocss, + cartocss_version: '2.3.0' + } + } + ] + }; +} + +function createRequest(layergroup, userHost, apiKey) { + var url = layergroupUrl; + if (apiKey) { + url += '?api_key=' + apiKey; + } + return { + url: url, + method: 'POST', + headers: { + host: userHost || 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(layergroup) + }; +} + +var layergroupUrl = '/api/v1/map'; +var pointSqlMaster = "select * from test_table_private_1"; +var pointSqlPublic = "select * from test_table"; +var keysToDelete; + +describe('authorization fallback', function () { + beforeEach(function () { + keysToDelete = {}; + }); + + afterEach(function (done) { + testHelper.deleteRedisKeys(keysToDelete, done); + }); + + it("succeed with master", function (done) { + var layergroup = singleLayergroupConfig(pointSqlMaster, '#layer { marker-fill:red; }'); + + assert.response(server, + createRequest(layergroup, 'user_previous_to_project_auth', '4444'), + { + status: 200 + }, + function (res, err) { + assert.ifError(err); + + var parsed = JSON.parse(res.body); + assert.ok(parsed.layergroupid); + assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid); + + keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0; + keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5; + + done(); + } + ); + }); + + + it("succeed with default - sending default_public", function (done) { + var layergroup = singleLayergroupConfig(pointSqlPublic, '#layer { marker-fill:red; }'); + + assert.response(server, + createRequest(layergroup, 'user_previous_to_project_auth', 'default_public'), + { + status: 200 + }, + function (res, err) { + assert.ifError(err); + + var parsed = JSON.parse(res.body); + assert.ok(parsed.layergroupid); + assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid); + + keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0; + keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5; + + done(); + } + ); + }); + + it("succeed with default - sending no api key token", function (done) { + var layergroup = singleLayergroupConfig(pointSqlPublic, '#layer { marker-fill:red; }'); + + assert.response(server, + createRequest(layergroup, 'user_previous_to_project_auth'), + { + status: 200 + }, + function (res, err) { + assert.ifError(err); + + var parsed = JSON.parse(res.body); + assert.ok(parsed.layergroupid); + assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid); + + keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0; + keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5; + + done(); + } + ); + }); + + it("succeed with non-existent api key - defaults to default", function (done) { + var layergroup = singleLayergroupConfig(pointSqlPublic, '#layer { marker-fill:red; }'); + + assert.response(server, + createRequest(layergroup, 'user_previous_to_project_auth', 'THIS-API-KEY-DOESNT-EXIST'), + { + status: 200 + }, + function (res, err) { + assert.ifError(err); + + var parsed = JSON.parse(res.body); + assert.ok(parsed.layergroupid); + assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid); + + keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0; + keysToDelete['user:user_previous_to_project_auth:mapviews:global'] = 5; + + done(); + } + ); + }); + + it("fail with default", function (done) { + var layergroup = singleLayergroupConfig(pointSqlMaster, '#layer { marker-fill:red; }'); + + assert.response(server, + createRequest(layergroup, 'user_previous_to_project_auth', 'default_public'), + { + status: 403 + }, + function (res, err) { + assert.ifError(err); + + done(); + } + ); + }); + + it("fail with non-existent api key - defaults to default", function (done) { + var layergroup = singleLayergroupConfig(pointSqlMaster, '#layer { marker-fill:red; }'); + + assert.response(server, + createRequest(layergroup, 'user_previous_to_project_auth', 'THIS-API-KEY-DOESNT-EXIST'), + { + status: 403 + }, + function (res, err) { + assert.ifError(err); + + done(); + } + ); + }); +}); diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index 56fcc3c9..59405389 100755 --- a/test/support/prepare_db.sh +++ b/test/support/prepare_db.sh @@ -131,6 +131,19 @@ HMSET rails:users:cartodb250user id ${TESTUSERID} \ map_key 4321 EOF + +# Remove this block when Auth fallback is not used anymore +# AUTH_FALLBACK + # A user to test auth fallback to no api keys mode + cat <