From ea6e8b531531a239f0850fe43892a5beebc3e736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa=20Aubert?= Date: Wed, 14 Feb 2018 16:22:36 +0100 Subject: [PATCH] Implement fallback mechanism to be able to authenticate as usual in case of apikey is not found --- app/services/user_database_service.js | 150 ++++++++++++++------------ test/acceptance/auth-api.js | 64 ++++++++--- test/prepare_db.sh | 10 ++ test/support/sql/test.sql | 6 ++ 4 files changed, 147 insertions(+), 83 deletions(-) diff --git a/app/services/user_database_service.js b/app/services/user_database_service.js index 9667643d..0b98b3b6 100644 --- a/app/services/user_database_service.js +++ b/app/services/user_database_service.js @@ -3,7 +3,7 @@ var step = require('step'); var _ = require('underscore'); -function isValidApiKey(apikey) { +function isApiKeyFound(apikey) { return apikey.type !== null && apikey.user !== null && apikey.databasePassword !== null && @@ -27,7 +27,6 @@ function UserDatabaseService(metadataBackend) { UserDatabaseService.prototype.getConnectionParams = function (authApi, cdbUsername, callback) { var self = this; - var dbParams; var dbopts = { port: global.settings.db_port, pass: global.settings.db_pubuser_pass @@ -40,9 +39,7 @@ UserDatabaseService.prototype.getConnectionParams = function (authApi, cdbUserna function getDatabaseConnectionParams() { self.metadataBackend.getAllUserDBParams(cdbUsername, this); }, - function authenticate(err, userDBParams) { - var next = this; - + function getApiKey (err, dbParams) { if (err) { err.http_status = 404; err.message = "Sorry, we can't find CartoDB user '" + cdbUsername + "'. " + @@ -50,18 +47,93 @@ UserDatabaseService.prototype.getConnectionParams = function (authApi, cdbUserna return callback(err); } - dbParams = userDBParams; + const next = this; + + if (authApi.getType() !== 'apiKey') { + return next(null, dbopts, dbParams); + } + + const apikeyToken = authApi.getCredentials(); + + self.metadataBackend.getApikey(cdbUsername, apikeyToken, (err, apikey) => { + if (err) { + return next(err); + } + + if (!isApiKeyFound(apikey)) { + return next(null, dbopts, dbParams); + } + + if (!apikey.grantsSql) { + const forbiddenError = new Error('forbidden'); + forbiddenError.http_status = 403; + + return next(forbiddenError); + } + + dbParams.apikey = apikeyToken; + + next(null, dbopts, dbParams, apikey); + }); + }, + function authenticate(err, dbopts, dbParams, apikey) { + var next = this; + + if (err) { + return next(err); + } dbopts.host = dbParams.dbhost; dbopts.dbname = dbParams.dbname; dbopts.user = (!!dbParams.dbpublicuser) ? dbParams.dbpublicuser : global.settings.db_pubuser; - authApi.verifyCredentials({ + const opts = { metadataBackend: self.metadataBackend, apiKey: dbParams.apikey - }, next); + }; + + + authApi.verifyCredentials(opts, function (err, isAuthenticated) { + if (err) { + return next(err); + } + + next(null, isAuthenticated, dbopts, dbParams, apikey); + }); }, - function getUserLimits (err, isAuthenticated) { + function setDBAuth(err, isAuthenticated, dbopts, dbParams, apikey) { + const next = this; + + if (err) { + return next(err); + } + + var user = _.template(global.settings.db_user, {user_id: dbParams.dbuser}); + var pass = null; + + if (global.settings.hasOwnProperty('db_user_pass')) { + pass = _.template(global.settings.db_user_pass, { + user_id: dbParams.dbuser, + user_password: dbParams.dbpass + }); + } + + if (isAuthenticated) { + dbopts.authenticated = isAuthenticated; + if (apikey) { + dbopts.user = apikey.databaseRole; + dbopts.pass = apikey.databasePassword; + } else { + dbopts.user = user; + dbopts.pass = pass; + } + } + + var authDbOpts = _.defaults({ user: user, pass: pass }, dbopts); + + return next(null, isAuthenticated, dbopts, authDbOpts); + }, + function getUserLimits (err, isAuthenticated, dbopts, authDbOpts) { var next = this; if (err) { @@ -77,66 +149,6 @@ UserDatabaseService.prototype.getConnectionParams = function (authApi, cdbUserna timeout: isAuthenticated ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic }; - next(null, isAuthenticated, userLimits); - }); - }, - function setDBAuth(err, isAuthenticated, userLimits) { - if (err) { - throw err; - } - - var user = _.template(global.settings.db_user, {user_id: dbParams.dbuser}); - var pass = null; - if (global.settings.hasOwnProperty('db_user_pass')) { - pass = _.template(global.settings.db_user_pass, { - user_id: dbParams.dbuser, - user_password: dbParams.dbpass - }); - } - - if (_.isBoolean(isAuthenticated) && isAuthenticated) { - dbopts.authenticated = isAuthenticated; - dbopts.user = user; - dbopts.pass = pass; - } - - var authDbOpts = _.defaults({user: user, pass: pass}, dbopts); - - return this(null, dbopts, authDbOpts, userLimits); - }, - function getApiKey (err, dbopts, authDbOpts, userLimits) { - if (err) { - throw err; - } - const next = this; - - if (authApi.getType() !== 'apiKey') { - return next(null, dbopts, authDbOpts, userLimits); - } - - self.metadataBackend.getApikey(cdbUsername, authApi.getCredentials(), (err, apiKey) => { - if (err) { - return next(err); - } - - if (!isValidApiKey(apiKey)) { - const unauthorizedError = new Error('permission denied'); - unauthorizedError.http_status = 401; - - return next(unauthorizedError); - } - - if (!apiKey.grantsSql) { - const forbiddenError = new Error('forbidden'); - forbiddenError.http_status = 403; - - return next(forbiddenError); - } - - if (apiKey.type !== 'default') { - dbopts = _.extend(dbopts, { user: apiKey.databaseRole, pass: apiKey.databasePassword }); - } - next(null, dbopts, authDbOpts, userLimits); }); }, diff --git a/test/acceptance/auth-api.js b/test/acceptance/auth-api.js index bbf56ee3..b72f1f76 100644 --- a/test/acceptance/auth-api.js +++ b/test/acceptance/auth-api.js @@ -1,6 +1,7 @@ const assert = require('../support/assert'); const TestClient = require('../support/test-client'); const BatchTestClient = require('../support/batch-test-client'); +const JobStatus = require('../../batch/job_status'); describe('Auth API', function () { const publicSQL = 'select * from untitle_table_4'; @@ -16,7 +17,7 @@ describe('Auth API', function () { }); }); - it.only('should fail while fetching data (private dataset) and using the default API key', function (done) { + it('should fail while fetching data (private dataset) and using the default API key', function (done) { this.testClient = new TestClient(); const expectedResponse = { response: { @@ -59,7 +60,7 @@ describe('Auth API', function () { }); }); - it('should fail while fetching data (scoped dataset) and using the default API key', function (done) { + it('should fail while fetching data (scoped dataset) and using regular API key', function (done) { this.testClient = new TestClient({ apiKey: 'regular2' }); const expectedResponse = { response: { @@ -74,21 +75,56 @@ describe('Auth API', function () { }); }); - it('should fail while creating a job with regular api key', function (done) { - this.testClient = new BatchTestClient({ apiKey: 'regular1' }); - const expectedResponse = { - response: { - status: 401 - } - }; + describe('Fallback', function () { + it('should get result from query using master apikey (fallback) and a granted dataset', function (done) { + this.testClient = new TestClient({ apiKey: '4321', host: 'cartofante.cartodb.com' }); + this.testClient.getResult(scopedSQL, (err, result) => { + assert.ifError(err); + assert.equal(result.length, 4); + done(); + }); + }); - this.testClient.createJob({ query: scopedSQL }, expectedResponse, (err, jobResult) => { - if (err) { - return done(err); - } + it('should fail while getting result from query using metadata and scoped dataset', function (done) { + this.testClient = new TestClient({ host: 'cartofante.cartodb.com' }); - assert.deepEqual(jobResult.job.error, [ 'permission denied' ]); + const expectedResponse = { + response: { + status: 401 + }, + anonymous: true + }; + this.testClient.getResult(privateSQL, expectedResponse, (err, result) => { + assert.ifError(err); + assert.equal(result.error, 'permission denied for relation private_table'); + done(); + }); + }); + }); + + describe('Batch API', function () { + it('should create while creating a job with regular api key', function (done) { + this.testClient = new BatchTestClient({ apiKey: 'regular1' }); + + this.testClient.createJob({ query: scopedSQL }, (err, jobResult) => { + if (err) { + return done(err); + } + + jobResult.getStatus(function (err, job) { + if (err) { + return done(err); + } + + assert.equal(job.status, JobStatus.DONE); + + done(); + }); + }); + }); + + afterEach(function (done) { this.testClient.drain(done); }); }); diff --git a/test/prepare_db.sh b/test/prepare_db.sh index 8eda81c0..e7d1400b 100755 --- a/test/prepare_db.sh +++ b/test/prepare_db.sh @@ -155,6 +155,16 @@ HMSET rails:oauth_access_tokens:l0lPbtP68ao8NfStCiA3V3neqfM03JKhToxhUQTR \ time sometime EOF + cat <