diff --git a/NEWS.md b/NEWS.md index 64305a89..712aacd9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,10 @@ # Changelog -## 5.3.2 +## 5.4.0 Released yyyy-mm-dd - Upgrades Windshaft to 4.5.3 + - Implemented middleware to authorize users via new Api Key system + - Keep the old authorization system as fallback ## 5.3.1 Released 2018-02-13 diff --git a/lib/cartodb/api/auth_api.js b/lib/cartodb/api/auth_api.js index 562614c3..8782f907 100644 --- a/lib/cartodb/api/auth_api.js +++ b/lib/cartodb/api/auth_api.js @@ -1,5 +1,4 @@ -var assert = require('assert'); -var step = require('step'); +var _ = require('underscore'); // AUTH_FALLBACK /** * @@ -47,39 +46,113 @@ AuthApi.prototype.authorizedBySigner = function(res, callback) { }); }; +function isValidApiKey(apikey) { + return apikey.type && + apikey.user && + apikey.databasePassword && + apikey.databaseRole; +} + // Check if a request is authorized by api_key // // @param user -// @param req express request object +// @param res express response object // @param callback function(err, authorized) // NOTE: authorized is expected to be 0 or 1 (integer) // -AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) { - var givenKey = req.query.api_key || req.query.map_key; - if ( ! givenKey && req.body ) { - // check also in request body - givenKey = req.body.api_key || req.body.map_key; - } - if ( ! givenKey ) { - return callback(null, 0); // no api key, no authorization... +AuthApi.prototype.authorizedByAPIKey = function(user, res, callback) { + const apikeyToken = res.locals.api_key; + const apikeyUsername = res.locals.apikeyUsername; + + if ( ! apikeyToken ) { + return callback(null, false); // no api key, no authorization... } - var self = this; + this.metadataBackend.getApikey(user, apikeyToken, (err, apikey) => { + if (err) { + if (isNameNotFoundError(err)) { + err.http_status = 404; + } - step( - function () { - self.metadataBackend.getUserMapKey(user, this); - }, - function checkApiKey(err, val){ - assert.ifError(err); - return val && givenKey === val; - }, - function finish(err, authorized) { - callback(err, authorized); + 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'; + error.subtype = 'api-key-not-found'; + error.http_status = 401; + + return callback(error); + } + + 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); + } + + if (!apikey.grantsMaps) { + const error = new Error('Forbidden'); + error.type = 'auth'; + error.subtype = 'api-key-does-not-grant-access'; + error.http_status = 403; + + return callback(error); + } + + return callback(null, true); + }); }; +//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 * @@ -88,51 +161,57 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) { * @param callback function(err, allowed) is access allowed not? */ AuthApi.prototype.authorize = function(req, res, callback) { - var self = this; var user = res.locals.user; - step( - function () { - self.authorizedByAPIKey(user, req, this); - }, - function checkApiKey(err, authorized){ - req.profiler.done('authorizedByAPIKey'); - assert.ifError(err); + this.authorizedByAPIKey(user, res, (err, isAuthorizedByApikey) => { + if (err) { + return callback(err); + } - // if not authorized by api_key, continue - if (!authorized) { - // not authorized by api_key, check if authorized by signer - return self.authorizedBySigner(res, this); - } + if (isAuthorizedByApikey) { + return this.pgConnection.setDBAuth(user, res.locals, 'regular', function (err) { + req.profiler.done('setDBAuth'); - // authorized by api key, login as the given username and stop - self.pgConnection.setDBAuth(user, res.locals, function(err) { - callback(err, true); // authorized (or error) + if (err) { + return callback(err); + } + + callback(null, true); }); - }, - function checkSignAuthorized(err, authorized) { + } + + this.authorizedBySigner(res, (err, isAuthorizedBySigner) => { if (err) { return callback(err); } - if ( ! authorized ) { - // request not authorized by signer. + if (isAuthorizedBySigner) { + return this.pgConnection.setDBAuth(user, res.locals, 'master', function (err) { + req.profiler.done('setDBAuth'); - // if no signer name was given, let dbparams and - // PostgreSQL do the rest. - // - if ( ! res.locals.signer ) { - return callback(null, true); // authorized so far - } + if (err) { + return callback(err); + } - // if signer name was given, return no authorization - return callback(null, false); + callback(null, true); + }); } - self.pgConnection.setDBAuth(user, res.locals, function(err) { - req.profiler.done('setDBAuth'); - callback(err, true); // authorized (or error) - }); - } - ); + // if no signer name was given, use default api key + if (!res.locals.signer) { + return this.pgConnection.setDBAuth(user, res.locals, 'default', function (err) { + req.profiler.done('setDBAuth'); + + if (err) { + return callback(err); + } + + callback(null, true); + }); + } + + // if signer name was given, return no authorization + return callback(null, false); + }); + }); }; diff --git a/lib/cartodb/backends/pg_connection.js b/lib/cartodb/backends/pg_connection.js index d98bb703..f389c687 100644 --- a/lib/cartodb/backends/pg_connection.js +++ b/lib/cartodb/backends/pg_connection.js @@ -1,7 +1,6 @@ -var assert = require('assert'); -var step = require('step'); var PSQL = require('cartodb-psql'); var _ = require('underscore'); +const debug = require('debug')('cachechan'); function PgConnection(metadataBackend) { this.metadataBackend = metadataBackend; @@ -20,45 +19,85 @@ module.exports = PgConnection; // // @param callback function(err) // -PgConnection.prototype.setDBAuth = function(username, params, callback) { - var self = this; - - var user_params = {}; - var auth_user = global.environment.postgres_auth_user; - var auth_pass = global.environment.postgres_auth_pass; - step( - function getId() { - self.metadataBackend.getUserId(username, this); - }, - function(err, user_id) { - assert.ifError(err); - user_params.user_id = user_id; - var dbuser = _.template(auth_user, user_params); - _.extend(params, {dbuser:dbuser}); - - // skip looking up user_password if postgres_auth_pass - // doesn't contain the "user_password" label - if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) { - return null; +PgConnection.prototype.setDBAuth = function(username, params, apikeyType, callback) { + if (apikeyType === 'master') { + this.metadataBackend.getMasterApikey(username, (err, apikey) => { + if (err) { + if (isNameNotFoundError(err)) { + err.http_status = 404; + } + return callback(err); } - self.metadataBackend.getUserDBPass(username, this); - }, - function(err, user_password) { - assert.ifError(err); - user_params.user_password = user_password; - if ( auth_pass ) { - var dbpass = _.template(auth_pass, user_params); - _.extend(params, {dbpassword:dbpass}); + 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 true; - }, - function finish(err) { - callback(err); - } - ); + + return callback(); + }); + } 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; + + //Remove this block when Auth fallback is not used anymore + // AUTH_FALLBACK + // master apikey has been recreated from user's metadata + 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 + // default apikey has been recreated from user's metadata + if (!params.dbpassword && global.environment.postgres.password) { + params.dbpassword = global.environment.postgres.password; + } + + 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 { + return callback(new Error(`Invalid Apikey type: ${apikeyType}, valid ones: master, regular, default`)); + } }; +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, @@ -71,36 +110,30 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) { // @param callback function(err) // PgConnection.prototype.setDBConn = function(dbowner, params, callback) { - var self = this; - // Add default database connection parameters - // if none given _.defaults(params, { - dbuser: global.environment.postgres.user, - dbpassword: global.environment.postgres.password, + // dbuser: global.environment.postgres.user, + // dbpassword: global.environment.postgres.password, dbhost: global.environment.postgres.host, dbport: global.environment.postgres.port }); - step( - function getConnectionParams() { - self.metadataBackend.getUserDBConnectionParams(dbowner, this); - }, - function extendParams(err, dbParams){ - assert.ifError(err); - // we don't want null values or overwrite a non public user - if (params.dbuser !== 'publicuser' || !dbParams.dbuser) { - delete dbParams.dbuser; - } - if ( dbParams ) { - _.extend(params, dbParams); - } - return null; - }, - function finish(err) { - callback(err); - } - ); -}; + this.metadataBackend.getUserDBConnectionParams(dbowner, (err, dbParams) => { + if (err) { + return callback(err); + } + + // we don’t want null values or overwrite a non public user + if (params.dbuser !== 'publicuser' || !dbParams.dbuser) { + delete dbParams.dbuser; + } + + if (dbParams) { + _.extend(params, dbParams); + } + + callback(); + }); +}; /** * Returns a `cartodb-psql` object for a given username. @@ -109,28 +142,37 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) { */ PgConnection.prototype.getConnection = function(username, callback) { - var self = this; + debug("getConn1"); - var params = {}; - - require('debug')('cachechan')("getConn1"); - step( - function setAuth() { - self.setDBAuth(username, params, this); - }, - function setConn(err) { - assert.ifError(err); - self.setDBConn(username, params, this); - }, - function openConnection(err) { - assert.ifError(err); - return callback(err, new PSQL({ - user: params.dbuser, - pass: params.dbpass, - host: params.dbhost, - port: params.dbport, - dbname: params.dbname - })); + this.getDatabaseParams(username, (err, databaseParams) => { + if (err) { + return callback(err); } - ); + return callback(err, new PSQL({ + user: databaseParams.dbuser, + pass: databaseParams.dbpass, + host: databaseParams.dbhost, + port: databaseParams.dbport, + dbname: databaseParams.dbname + })); + + }); +}; + +PgConnection.prototype.getDatabaseParams = function(username, callback) { + const databaseParams = {}; + + this.setDBAuth(username, databaseParams, 'master', err => { + if (err) { + return callback(err); + } + + this.setDBConn(username, databaseParams, err => { + if (err) { + return callback(err); + } + + callback(null, databaseParams); + }); + }); }; diff --git a/lib/cartodb/backends/pg_query_runner.js b/lib/cartodb/backends/pg_query_runner.js index bf57f166..d3a16719 100644 --- a/lib/cartodb/backends/pg_query_runner.js +++ b/lib/cartodb/backends/pg_query_runner.js @@ -1,6 +1,4 @@ -var assert = require('assert'); var PSQL = require('cartodb-psql'); -var step = require('step'); function PgQueryRunner(pgConnection) { this.pgConnection = pgConnection; @@ -16,31 +14,23 @@ module.exports = PgQueryRunner; * @param {Function} callback function({Error}, {Array}) second argument is guaranteed to be an array */ PgQueryRunner.prototype.run = function(username, query, callback) { - var self = this; - var params = {}; - - step( - function setAuth() { - self.pgConnection.setDBAuth(username, params, this); - }, - function setConn(err) { - assert.ifError(err); - self.pgConnection.setDBConn(username, params, this); - }, - function executeQuery(err) { - assert.ifError(err); - var psql = new PSQL({ - user: params.dbuser, - pass: params.dbpass, - host: params.dbhost, - port: params.dbport, - dbname: params.dbname - }); - psql.query(query, function(err, resultSet) { - resultSet = resultSet || {}; - return callback(err, resultSet.rows || []); - }); + this.pgConnection.getDatabaseParams(username, (err, databaseParams) => { + if (err) { + return callback(err); } - ); + + const psql = new PSQL({ + user: databaseParams.dbuser, + pass: databaseParams.dbpass, + host: databaseParams.dbhost, + port: databaseParams.dbport, + dbname: databaseParams.dbname + }); + + psql.query(query, function (err, resultSet) { + resultSet = resultSet || {}; + return callback(err, resultSet.rows || []); + }); + }); }; diff --git a/lib/cartodb/controllers/layergroup.js b/lib/cartodb/controllers/layergroup.js index 7257164d..5ad97e52 100644 --- a/lib/cartodb/controllers/layergroup.js +++ b/lib/cartodb/controllers/layergroup.js @@ -306,7 +306,7 @@ LayergroupController.prototype.tileOrLayer = function (req, res, next) { function mapController$getTileOrGrid() { self.tileBackend.getTile( new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals), - req.params, this + res.locals, this ); }, function mapController$finalize(err, tile, headers, stats) { diff --git a/lib/cartodb/controllers/named_maps_admin.js b/lib/cartodb/controllers/named_maps_admin.js index 19e8c53a..f1f2c85b 100644 --- a/lib/cartodb/controllers/named_maps_admin.js +++ b/lib/cartodb/controllers/named_maps_admin.js @@ -3,6 +3,13 @@ const cors = require('../middleware/cors'); const userMiddleware = require('../middleware/user'); const rateLimit = require('../middleware/rate-limit'); const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit; +const localsMiddleware = require('../middleware/context/locals'); +const apikeyCredentialsMiddleware = require('../middleware/context/apikey-credentials'); + +const apikeyMiddleware = [ + localsMiddleware, + apikeyCredentialsMiddleware(), +]; /** * @param {AuthApi} authApi @@ -26,6 +33,7 @@ NamedMapsAdminController.prototype.register = function (app) { cors(), userMiddleware, rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_CREATE), + apikeyMiddleware, this.checkContentType('POST', 'POST TEMPLATE'), this.authorizedByAPIKey('create', 'POST TEMPLATE'), this.create() @@ -36,6 +44,7 @@ NamedMapsAdminController.prototype.register = function (app) { cors(), userMiddleware, rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_UPDATE), + apikeyMiddleware, this.checkContentType('PUT', 'PUT TEMPLATE'), this.authorizedByAPIKey('update', 'PUT TEMPLATE'), this.update() @@ -46,6 +55,7 @@ NamedMapsAdminController.prototype.register = function (app) { cors(), userMiddleware, rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_GET), + apikeyMiddleware, this.authorizedByAPIKey('get', 'GET TEMPLATE'), this.retrieve() ); @@ -55,6 +65,7 @@ NamedMapsAdminController.prototype.register = function (app) { cors(), userMiddleware, rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_DELETE), + apikeyMiddleware, this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'), this.destroy() ); @@ -64,6 +75,7 @@ NamedMapsAdminController.prototype.register = function (app) { cors(), userMiddleware, rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_LIST), + apikeyMiddleware, this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'), this.list() ); @@ -77,8 +89,7 @@ NamedMapsAdminController.prototype.register = function (app) { NamedMapsAdminController.prototype.authorizedByAPIKey = function (action, label) { return function authorizedByAPIKeyMiddleware (req, res, next) { const { user } = res.locals; - - this.authApi.authorizedByAPIKey(user, req, (err, authenticated) => { + this.authApi.authorizedByAPIKey(user, res, (err, authenticated) => { if (err) { return next(err); } diff --git a/lib/cartodb/middleware/context/apikey-credentials.js b/lib/cartodb/middleware/context/apikey-credentials.js new file mode 100644 index 00000000..225c8ab6 --- /dev/null +++ b/lib/cartodb/middleware/context/apikey-credentials.js @@ -0,0 +1,86 @@ +'use strict'; + +module.exports = function apikeyToken () { + return function apikeyTokenMiddleware(req, res, next) { + const apikeyCredentials = getApikeyCredentialsFromRequest(req); + res.locals.api_key = apikeyCredentials.token; + res.locals.apikeyUsername = apikeyCredentials.username; + return next(); + }; +}; + +//-------------------------------------------------------------------------------- + +const basicAuth = require('basic-auth'); + +function getApikeyCredentialsFromRequest(req) { + let apikeyCredentials = { + token: null, + username: null, + }; + + for (let getter of apikeyGetters) { + apikeyCredentials = getter(req); + if (apikeyTokenFound(apikeyCredentials)) { + break; + } + } + + return apikeyCredentials; +} + +const apikeyGetters = [ + getApikeyTokenFromHeaderAuthorization, + getApikeyTokenFromRequestQueryString, + getApikeyTokenFromRequestBody, +]; + +function getApikeyTokenFromHeaderAuthorization(req) { + const credentials = basicAuth(req); + + if (credentials) { + return { + username: credentials.username, + token: credentials.pass + }; + } else { + return { + username: null, + token: null, + }; + } +} + +function getApikeyTokenFromRequestQueryString(req) { + let token = null; + + if (req.query && req.query.api_key) { + token = req.query.api_key; + } else if (req.query && req.query.map_key) { + token = req.query.map_key; + } + + return { + username: null, + token: token, + }; +} + +function getApikeyTokenFromRequestBody(req) { + let token = null; + + if (req.body && req.body.api_key) { + token = req.body.api_key; + } else if (req.body && req.body.map_key) { + token = req.body.map_key; + } + + return { + username: null, + token: token, + }; +} + +function apikeyTokenFound(apikey) { + return !!apikey && !!apikey.token; +} diff --git a/lib/cartodb/middleware/context/db-conn-setup.js b/lib/cartodb/middleware/context/db-conn-setup.js index dec48f6d..068d77c2 100644 --- a/lib/cartodb/middleware/context/db-conn-setup.js +++ b/lib/cartodb/middleware/context/db-conn-setup.js @@ -12,8 +12,6 @@ module.exports = function dbConnSetupMiddleware(pgConnection) { return next(err); } - // Add default database connection parameters - // if none given _.defaults(res.locals, { dbuser: global.environment.postgres.user, dbpassword: global.environment.postgres.password, diff --git a/lib/cartodb/middleware/context/index.js b/lib/cartodb/middleware/context/index.js index 640aafd6..8922739f 100644 --- a/lib/cartodb/middleware/context/index.js +++ b/lib/cartodb/middleware/context/index.js @@ -1,6 +1,7 @@ const locals = require('./locals'); const cleanUpQueryParams = require('./clean-up-query-params'); const layergroupToken = require('./layergroup-token'); +const apikeyCredentials = require('./apikey-credentials'); const authorize = require('./authorize'); const dbConnSetup = require('./db-conn-setup'); @@ -9,6 +10,7 @@ module.exports = function prepareContextMiddleware(authApi, pgConnection) { locals, cleanUpQueryParams(), layergroupToken, + apikeyCredentials(), authorize(authApi), dbConnSetup(pgConnection) ]; diff --git a/lib/cartodb/models/mapconfig/adapter/mapconfig-named-layers-adapter.js b/lib/cartodb/models/mapconfig/adapter/mapconfig-named-layers-adapter.js index 17c2059f..f652e675 100644 --- a/lib/cartodb/models/mapconfig/adapter/mapconfig-named-layers-adapter.js +++ b/lib/cartodb/models/mapconfig/adapter/mapconfig-named-layers-adapter.js @@ -108,8 +108,7 @@ MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapC var dbAuth = {}; if (_.some(layers, isNamedTypeLayer)) { - // Lazy load dbAuth - this.pgConnection.setDBAuth(user, dbAuth, function(err) { + this.pgConnection.setDBAuth(user, dbAuth, 'master', function(err) { if (err) { return callback(err); } diff --git a/lib/cartodb/models/mapconfig/provider/named-map-provider.js b/lib/cartodb/models/mapconfig/provider/named-map-provider.js index 17c66c29..9b085dbc 100644 --- a/lib/cartodb/models/mapconfig/provider/named-map-provider.js +++ b/lib/cartodb/models/mapconfig/provider/named-map-provider.js @@ -232,19 +232,19 @@ function configHash(config) { module.exports.configHash = configHash; NamedMapMapConfigProvider.prototype.setDBParams = function(cdbuser, params, callback) { - var self = this; - step( - function setAuth() { - self.pgConnection.setDBAuth(cdbuser, params, this); - }, - function setConn(err) { - assert.ifError(err); - self.pgConnection.setDBConn(cdbuser, params, this); - }, - function finish(err) { - callback(err); + this.pgConnection.getDatabaseParams(cdbuser, (err, databaseParams) => { + if (err) { + return callback(err); } - ); + + params.dbuser = databaseParams.dbuser; + params.dbpass = databaseParams.dbpass; + params.dbhost = databaseParams.dbhost; + params.dbport = databaseParams.dbport; + params.dbname = databaseParams.dbname; + + callback(); + }); }; NamedMapMapConfigProvider.prototype.getTemplateName = function() { diff --git a/package.json b/package.json index 09bfb4a8..8b87d90a 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,12 @@ "Simon Martin " ], "dependencies": { + "basic-auth": "^2.0.0", "body-parser": "^1.18.2", "camshaft": "0.61.2", "cartodb-psql": "0.10.2", "cartodb-query-tables": "0.3.0", - "cartodb-redis": "0.15.0", + "cartodb-redis": "0.16.0", "debug": "^3.1.0", "dot": "~1.0.2", "express": "~4.16.0", diff --git a/test/acceptance/analysis/error-cases.js b/test/acceptance/analysis/error-cases.js index 376d7606..2e2ea922 100644 --- a/test/acceptance/analysis/error-cases.js +++ b/test/acceptance/analysis/error-cases.js @@ -188,7 +188,7 @@ describe('analysis-layers error cases', function() { ] ); - var testClient = new TestClient(mapConfig, 11111); + var testClient = new TestClient(mapConfig); //No apikey provided -> using default public apikey testClient.getLayergroup({ response: AUTH_ERROR_RESPONSE }, function(err, layergroupResult) { assert.ok(!err, err); 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/acceptance/auth/authorization.js b/test/acceptance/auth/authorization.js new file mode 100644 index 00000000..d7df8b32 --- /dev/null +++ b/test/acceptance/auth/authorization.js @@ -0,0 +1,431 @@ +require('../../support/test_helper'); + +const assert = require('../../support/assert'); +const TestClient = require('../../support/test-client'); +const mapnik = require('windshaft').mapnik; + +const PERMISSION_DENIED_RESPONSE = { + status: 403, + headers: { + 'Content-Type': 'application/json; charset=utf-8' + } +}; + +describe('authorization', function() { + it('should create a layergroup with regular apikey token', function(done) { + const apikeyToken = 'regular1'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table_localhost_regular1', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getLayergroup(function (err, layergroupResult) { + assert.ifError(err); + + assert.ok(layergroupResult.layergroupid); + + testClient.drain(done); + }); + }); + + it('should create and get a named map tile using a regular apikey token', function (done) { + const apikeyToken = 'regular1'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table_localhost_regular1', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getTile(0, 0, 0, function (err, res, tile) { + assert.ifError(err); + + assert.equal(res.statusCode, 200); + assert.ok(tile instanceof mapnik.Image); + + testClient.drain(done); + }); + }); + + it('should fail getting a named map tile with default apikey token', function (done) { + const apikeyTokenCreate = 'regular1'; + const apikeyTokenGet = 'default_public'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table_localhost_regular1', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + + const testClientCreate = new TestClient(mapConfig, apikeyTokenCreate); + testClientCreate.getLayergroup(function (err, layergroupResult) { + assert.ifError(err); + const layergroupId = layergroupResult.layergroupid; + + const testClientGet = new TestClient({}, apikeyTokenGet); + + const params = { + layergroupid: layergroupId, + response: PERMISSION_DENIED_RESPONSE + }; + + testClientGet.getTile(0, 0, 0, params, function(err, res, body) { + + assert.ifError(err); + + assert.ok(body.hasOwnProperty('errors')); + assert.equal(body.errors.length, 1); + assert.ok(body.errors[0].match(/permission denied/), body.errors[0]); + + testClientGet.drain(done); + }); + }); + }); + + it('should fail creating a layergroup with default apikey token', function (done) { + const apikeyToken = 'default_public'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table_localhost_regular1', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getLayergroup({ response: { status: 403 } }, function (err, layergroupResult) { + assert.ifError(err); + + assert.ok(layergroupResult.hasOwnProperty('errors')); + assert.equal(layergroupResult.errors.length, 1); + assert.ok(layergroupResult.errors[0].match(/permission denied/), layergroupResult.errors[0]); + + testClient.drain(done); + }); + }); + + it('should create a layergroup with default apikey token', function (done) { + const apikeyToken = 'default_public'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getLayergroup(function (err, layergroupResult) { + assert.ifError(err); + + assert.ok(layergroupResult.layergroupid); + + testClient.drain(done); + }); + }); + + it('should create and get a tile with default apikey token', function (done) { + const apikeyToken = 'default_public'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getTile(0, 0, 0, function (err, res, tile) { + assert.ifError(err); + + assert.equal(res.statusCode, 200); + assert.ok(tile instanceof mapnik.Image); + + testClient.drain(done); + }); + }); + + it('should fail if apikey does not grant access to table', function (done) { + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table_localhost_regular1', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + const testClient = new TestClient(mapConfig); //no apikey provided, using default + + testClient.getLayergroup({ response: { status: 403 } }, function (err, layergroupResult) { //TODO 401 + assert.ifError(err); + + assert.ok(layergroupResult.hasOwnProperty('errors')); + assert.equal(layergroupResult.errors.length, 1); + assert.ok(layergroupResult.errors[0].match(/permission denied/), layergroupResult.errors[0]); + + testClient.drain(done); + }); + }); + + it('should forbide access to API if API key does not grant access', function (done) { + const apikeyToken = 'regular2'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + options: { + sql: 'select * FROM test_table_localhost_regular1', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getLayergroup({ response: { status: 403 } }, function (err, layergroupResult) { + assert.ifError(err); + + assert.ok(layergroupResult.hasOwnProperty('errors')); + assert.equal(layergroupResult.errors.length, 1); + assert.ok(layergroupResult.errors[0].match(/Forbidden/), layergroupResult.errors[0]); + + testClient.drain(done); + }); + }); + + it('should create a layergroup with a source analysis using a default apikey token', function (done) { + const apikeyToken = 'default_public'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + type: 'cartodb', + options: { + source: { + id: 'HEAD' + }, + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ], + analyses: [ + { + id: 'HEAD', + type: 'source', + params: { + query: 'select * from populated_places_simple_reduced' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getLayergroup(function (err, layergroupResult) { + assert.ifError(err); + + assert.ok(layergroupResult.layergroupid); + + testClient.drain(done); + }); + }); + + it('should create a layergroup with a source analysis using a regular apikey token', function (done) { + const apikeyToken = 'regular1'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + type: 'cartodb', + options: { + source: { + id: 'HEAD' + }, + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ], + analyses: [ + { + id: 'HEAD', + type: 'source', + params: { + query: 'select * from test_table_localhost_regular1' + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getLayergroup(function (err, layergroupResult) { + assert.ifError(err); + + assert.ok(layergroupResult.layergroupid); + + testClient.drain(done); + }); + }); + + // Warning: TBA + it('should create a layergroup with a buffer analysis using a regular apikey token', function (done) { + const apikeyToken = 'regular1'; + const mapConfig = { + version: '1.7.0', + layers: [ + { + type: 'cartodb', + options: { + source: { + id: 'HEAD1' + }, + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0' + } + } + ], + analyses: [ + { + id: "HEAD1", + type: "buffer", + params: { + source: { + id: 'HEAD2', + type: 'source', + params: { + query: 'select * from test_table_localhost_regular1' + } + }, + radius: 50000 + } + } + ] + }; + const testClient = new TestClient(mapConfig, apikeyToken); + + testClient.getLayergroup(function (err, layergroupResult) { + assert.ifError(err); + + assert.ok(layergroupResult.layergroupid); + + testClient.drain(done); + }); + }); + + it('should create and get a named map tile using a regular apikey token', function (done) { + const apikeyToken = 'regular1'; + + const template = { + version: '0.0.1', + name: 'auth-api-template', + placeholders: { + buffersize: { + type: 'number', + default: 0 + } + }, + layergroup: { + version: '1.7.0', + layers: [{ + type: 'cartodb', + options: { + sql: 'select * from test_table_localhost_regular1', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0', + } + }] + } + }; + + const testClient = new TestClient(template, apikeyToken); + + testClient.getTile(0, 0, 0, function (err, res, tile) { + assert.ifError(err); + + assert.equal(res.statusCode, 200); + assert.ok(tile instanceof mapnik.Image); + + testClient.drain(done); + }); + }); + + it('should fail creating a named map using a regular apikey token and a private table', function (done) { + const apikeyToken = 'regular1'; + + const template = { + version: '0.0.1', + name: 'auth-api-template-private', + placeholders: { + buffersize: { + type: 'number', + default: 0 + } + }, + layergroup: { + version: '1.7.0', + layers: [{ + type: 'cartodb', + options: { + sql: 'select * from populated_places_simple_reduced_private', + cartocss: TestClient.CARTOCSS.POINTS, + cartocss_version: '2.3.0', + } + }] + } + }; + + const testClient = new TestClient(template, apikeyToken); + + testClient.getTile(0, 0, 0, { response: PERMISSION_DENIED_RESPONSE }, function (err, res, body) { + assert.ifError(err); + + assert.ok(body.hasOwnProperty('errors')); + assert.equal(body.errors.length, 1); + assert.ok(body.errors[0].match(/permission denied/), body.errors[0]); + + testClient.drain(done); + }); + }); +}); diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index 03d97fc7..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 <