diff --git a/lib/cartodb/api/auth_api.js b/lib/cartodb/api/auth_api.js index 0b93e308..677ccc86 100644 --- a/lib/cartodb/api/auth_api.js +++ b/lib/cartodb/api/auth_api.js @@ -56,21 +56,19 @@ function isValidApiKey(apikey) { // 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 ) { +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... } - this.metadataBackend.getApikey(user, givenKey, (err, apikey) => { + this.metadataBackend.getApikey(user, apikeyToken, (err, apikey) => { if (err) { return callback(err); } @@ -98,6 +96,15 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) { return callback(error); } + if (apikeyUsername && (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'; @@ -121,7 +128,7 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) { AuthApi.prototype.authorize = function(req, res, callback) { var user = res.locals.user; - this.authorizedByAPIKey(user, req, (err, isAuthorizedByApikey) => { + this.authorizedByAPIKey(user, res, (err, isAuthorizedByApikey) => { if (err) { return callback(err); } diff --git a/lib/cartodb/backends/pg_connection.js b/lib/cartodb/backends/pg_connection.js index c3eff05b..e4f9700e 100644 --- a/lib/cartodb/backends/pg_connection.js +++ b/lib/cartodb/backends/pg_connection.js @@ -37,7 +37,7 @@ PgConnection.prototype.setDBAuth = function(username, params, apikeyType, callba return callback(); }); } else if (apikeyType === 'regular') { //Actually it can be any type of api key - this.metadataBackend.getApikey(username, params.api_key || params.map_key, (err, apikey) => { + this.metadataBackend.getApikey(username, params.api_key, (err, apikey) => { if (err) { return callback(err); } diff --git a/lib/cartodb/controllers/named_maps_admin.js b/lib/cartodb/controllers/named_maps_admin.js index a4d05467..b1c6a451 100644 --- a/lib/cartodb/controllers/named_maps_admin.js +++ b/lib/cartodb/controllers/named_maps_admin.js @@ -1,6 +1,13 @@ const { templateName } = require('../backends/template_maps'); const cors = require('../middleware/cors'); const userMiddleware = require('../middleware/user'); +const localsMiddleware = require('../middleware/context/locals'); +const apikeyCredentialsMiddleware = require('../middleware/context/apikey-credentials'); + +const apikeyMiddleware = [ + localsMiddleware, + apikeyCredentialsMiddleware(), +]; /** * @param {AuthApi} authApi @@ -22,6 +29,7 @@ NamedMapsAdminController.prototype.register = function (app) { `${base_url_templated}/`, cors(), userMiddleware, + apikeyMiddleware, this.checkContentType('POST', 'POST TEMPLATE'), this.authorizedByAPIKey('create', 'POST TEMPLATE'), this.create() @@ -31,6 +39,7 @@ NamedMapsAdminController.prototype.register = function (app) { `${base_url_templated}/:template_id`, cors(), userMiddleware, + apikeyMiddleware, this.checkContentType('PUT', 'PUT TEMPLATE'), this.authorizedByAPIKey('update', 'PUT TEMPLATE'), this.update() @@ -40,6 +49,7 @@ NamedMapsAdminController.prototype.register = function (app) { `${base_url_templated}/:template_id`, cors(), userMiddleware, + apikeyMiddleware, this.authorizedByAPIKey('get', 'GET TEMPLATE'), this.retrieve() ); @@ -48,6 +58,7 @@ NamedMapsAdminController.prototype.register = function (app) { `${base_url_templated}/:template_id`, cors(), userMiddleware, + apikeyMiddleware, this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'), this.destroy() ); @@ -56,6 +67,7 @@ NamedMapsAdminController.prototype.register = function (app) { `${base_url_templated}/`, cors(), userMiddleware, + apikeyMiddleware, this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'), this.list() ); @@ -69,8 +81,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..820bb8c6 --- /dev/null +++ b/lib/cartodb/middleware/context/apikey-credentials.js @@ -0,0 +1,10 @@ +'use strict'; + +const getApikeyCredentialsFromRequest = require('../lib/get_api_key_credentials_from_request'); + +module.exports = () => function apikeyTokenMiddleware(req, res, next) { + const apikeyCredentials = getApikeyCredentialsFromRequest(req); + res.locals.api_key = apikeyCredentials.token; + res.locals.apikeyUsername = apikeyCredentials.username; + return next(); +}; 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/middleware/lib/get_api_key_credentials_from_request.js b/lib/cartodb/middleware/lib/get_api_key_credentials_from_request.js new file mode 100644 index 00000000..397e972b --- /dev/null +++ b/lib/cartodb/middleware/lib/get_api_key_credentials_from_request.js @@ -0,0 +1,77 @@ +'use strict'; + +const basicAuth = require('basic-auth'); + +module.exports = function getApiKeyCredentialsFromRequest(req) { + let apikeyCredentials = { + token: null, + username: null, + }; + + for (var 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/package.json b/package.json index 563aca4c..b2dc6652 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "Simon Martin " ], "dependencies": { + "basic-auth": "^2.0.0", "body-parser": "^1.18.2", "camshaft": "0.61.2", "cartodb-psql": "0.10.2", diff --git a/test/unit/cartodb/prepare-context.test.js b/test/unit/cartodb/prepare-context.test.js index 61ba0c55..2eb7e890 100644 --- a/test/unit/cartodb/prepare-context.test.js +++ b/test/unit/cartodb/prepare-context.test.js @@ -10,6 +10,7 @@ var TemplateMaps = require('../../../lib/cartodb/backends/template_maps'); const cleanUpQueryParamsMiddleware = require('../../../lib/cartodb/middleware/context/clean-up-query-params'); const authorizeMiddleware = require('../../../lib/cartodb/middleware/context/authorize'); const dbConnSetupMiddleware = require('../../../lib/cartodb/middleware/context/db-conn-setup'); +const apikeyCredentialsMiddleware = require('../../../lib/cartodb/middleware/context/apikey-credentials'); const localsMiddleware = require('../../../lib/cartodb/middleware/context/locals'); var windshaft = require('windshaft'); @@ -23,6 +24,7 @@ describe('prepare-context', function() { let cleanUpQueryParams; let dbConnSetup; let authorize; + let setApikeyCredentials; before(function() { var redisPool = new RedisPool(global.environment.redis); @@ -35,6 +37,7 @@ describe('prepare-context', function() { cleanUpQueryParams = cleanUpQueryParamsMiddleware(); authorize = authorizeMiddleware(authApi); dbConnSetup = dbConnSetupMiddleware(pgConnection); + setApikeyCredentials = apikeyCredentialsMiddleware(); }); @@ -180,4 +183,66 @@ describe('prepare-context', function() { }); }); + describe('Set apikey token', function(){ + it('from query param', function (done) { + var req = { + headers: { + host: 'localhost' + }, + query: { + api_key: '1234', + } + }; + var res = {}; + setApikeyCredentials(prepareRequest(req), prepareResponse(res), function (err) { + if (err) { + return done(err); + } + var query = res.locals; + + assert.equal('1234', query.api_key); + done(); + }); + }); + + it('from body param', function (done) { + var req = { + headers: { + host: 'localhost' + }, + body: { + api_key: '1234', + } + }; + var res = {}; + setApikeyCredentials(prepareRequest(req), prepareResponse(res), function (err) { + if (err) { + return done(err); + } + var query = res.locals; + + assert.equal('1234', query.api_key); + done(); + }); + }); + + it('from http header', function (done) { + var req = { + headers: { + host: 'localhost', + authorization: 'Basic bG9jYWxob3N0OjEyMzQ=', // user: localhost, password: 1234 + } + }; + var res = {}; + setApikeyCredentials(prepareRequest(req), prepareResponse(res), function (err) { + if (err) { + return done(err); + } + var query = res.locals; + + assert.equal('1234', query.api_key); + done(); + }); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index d8a290a4..c556c94e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -143,6 +143,12 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +basic-auth@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.0.tgz#015db3f353e02e56377755f962742e8981e7bbba" + dependencies: + safe-buffer "5.1.1" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"