Merge pull request #489 from CartoDB/remove-auth-fallback

Remove auth fallback
This commit is contained in:
Eneko Lakasta 2018-06-11 12:34:54 +02:00 committed by GitHub
commit f08ad3becd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 310 additions and 335 deletions

View File

@ -1,29 +1,38 @@
/**
* this module allows to auth user using an pregenerated api key
*/
function ApikeyAuth(req, metadataBackend, username, apikey) {
function ApikeyAuth(req, metadataBackend, username, apikeyToken) {
this.req = req;
this.metadataBackend = metadataBackend;
this.username = username;
this.apikey = apikey;
this.apikeyToken = apikeyToken;
}
module.exports = ApikeyAuth;
function errorUserNotFoundMessageTemplate (user) {
return `Sorry, we can't find CARTO user '${user}'. Please check that you have entered the correct domain.`;
function usernameMatches(basicAuthUsername, requestUsername) {
return !(basicAuthUsername && (basicAuthUsername !== requestUsername));
}
ApikeyAuth.prototype.verifyCredentials = function (callback) {
this.metadataBackend.getApikey(this.username, this.apikey, (err, apikey) => {
this.metadataBackend.getApikey(this.username, this.apikeyToken, (err, apikey) => {
if (err) {
err.http_status = 404;
err.message = errorUserNotFoundMessageTemplate(this.username);
err.http_status = 500;
err.message = 'Unexpected error';
return callback(err);
}
if (isApiKeyFound(apikey)) {
if (!usernameMatches(apikey.user, this.username)) {
const usernameError = new Error('Forbidden');
usernameError.type = 'auth';
usernameError.subtype = 'api-key-username-mismatch';
usernameError.http_status = 403;
return callback(usernameError);
}
if (!apikey.grantsSql) {
const forbiddenError = new Error('forbidden');
forbiddenError.http_status = 403;
@ -31,33 +40,28 @@ ApikeyAuth.prototype.verifyCredentials = function (callback) {
return callback(forbiddenError);
}
return callback(null, verifyRequest(this.apikey, this.apikey));
}
return callback(null, getAuthorizationLevel(apikey));
} else {
const apiKeyNotFoundError = new Error('Unauthorized');
apiKeyNotFoundError.type = 'auth';
apiKeyNotFoundError.subtype = 'api-key-not-found';
apiKeyNotFoundError.http_status = 401;
// Auth API Fallback
this.metadataBackend.getAllUserDBParams(this.username, (err, dbParams) => {
if (err) {
err.http_status = 404;
err.message = errorUserNotFoundMessageTemplate(this.username);
return callback(err);
}
callback(null, verifyRequest(this.apikey, dbParams.apikey));
});
return callback(apiKeyNotFoundError);
}
});
};
ApikeyAuth.prototype.hasCredentials = function () {
return !!this.apikey;
return !!this.apikeyToken;
};
ApikeyAuth.prototype.getCredentials = function () {
return this.apikey;
return this.apikeyToken;
};
function verifyRequest(apikey, requiredApikey) {
return (apikey === requiredApikey && apikey !== 'default_public');
function getAuthorizationLevel(apikey) {
return apikey.type;
}
function isApiKeyFound(apikey) {

View File

@ -3,33 +3,33 @@ var ApiKeyAuth = require('./apikey'),
function AuthApi(req, requestParams) {
this.req = req;
this.authBacked = getAuthBackend(req, requestParams);
this.authBackend = getAuthBackend(req, requestParams);
this._hasCredentials = null;
}
AuthApi.prototype.getType = function () {
if (this.authBacked instanceof ApiKeyAuth) {
if (this.authBackend instanceof ApiKeyAuth) {
return 'apiKey';
} else if (this.authBacked instanceof OAuthAuth) {
} else if (this.authBackend instanceof OAuthAuth) {
return 'oAuth';
}
};
AuthApi.prototype.hasCredentials = function() {
if (this._hasCredentials === null) {
this._hasCredentials = this.authBacked.hasCredentials();
this._hasCredentials = this.authBackend.hasCredentials();
}
return this._hasCredentials;
};
AuthApi.prototype.getCredentials = function() {
return this.authBacked.getCredentials();
return this.authBackend.getCredentials();
};
AuthApi.prototype.verifyCredentials = function(callback) {
if (this.hasCredentials()) {
this.authBacked.verifyCredentials(callback);
this.authBackend.verifyCredentials(callback);
} else {
callback(null, false);
}

View File

@ -142,7 +142,8 @@ var oAuth = (function(){
}, false);
},
function finishValidation(err, hasValidSignature) {
return callback(err, hasValidSignature || null);
const authorizationLevel = hasValidSignature ? 'master' : null;
return callback(err, authorizationLevel);
}
);
};

View File

@ -51,13 +51,13 @@ JobController.prototype.route = function (app) {
function composeJobMiddlewares (metadataBackend, userDatabaseService, jobService, statsdClient, userLimitsService) {
return function jobMiddlewares (action, jobMiddleware, endpointGroup) {
const forceToBeAuthenticated = true;
const forceToBeMaster = true;
return [
initializeProfilerMiddleware('job'),
userMiddleware(),
userMiddleware(metadataBackend),
rateLimitsMiddleware(userLimitsService, endpointGroup),
authorizationMiddleware(metadataBackend, forceToBeAuthenticated),
authorizationMiddleware(metadataBackend, forceToBeMaster),
connectionParamsMiddleware(userDatabaseService),
jobMiddleware(jobService),
setServedByDBHostHeader(),

View File

@ -33,12 +33,14 @@ function QueryController(metadataBackend, userDatabaseService, tableCache, stats
QueryController.prototype.route = function (app) {
const { base_url } = global.settings;
const forceToBeMaster = false;
const queryMiddlewares = endpointGroup => {
return [
initializeProfilerMiddleware('query'),
userMiddleware(),
userMiddleware(this.metadataBackend),
rateLimitsMiddleware(this.userLimitsService, endpointGroup),
authorizationMiddleware(this.metadataBackend),
authorizationMiddleware(this.metadataBackend, forceToBeMaster),
connectionParamsMiddleware(this.userDatabaseService),
timeoutLimitsMiddleware(this.metadataBackend),
this.handleQuery.bind(this),
@ -68,7 +70,7 @@ QueryController.prototype.handleQuery = function (req, res, next) {
var filename = requestedFilename;
var requestedSkipfields = params.skipfields;
const { user: username, userDbParams: dbopts, authDbParams, userLimits, authenticated } = res.locals;
const { user: username, userDbParams: dbopts, authDbParams, userLimits, authorizationLevel } = res.locals;
var skipfields;
var dp = params.dp; // decimal point digits (defaults to 6)
@ -143,7 +145,7 @@ QueryController.prototype.handleQuery = function (req, res, next) {
var pg = new PSQL(authDbParams);
var skipCache = authenticated;
var skipCache = authorizationLevel === 'master';
self.queryTables.getAffectedTablesFromQuery(pg, sql, skipCache, function(err, result) {
if (err) {
@ -162,7 +164,7 @@ QueryController.prototype.handleQuery = function (req, res, next) {
}
checkAborted('setHeaders');
if(!pgEntitiesAccessValidator.validate(affectedTables, authenticated)) {
if(!pgEntitiesAccessValidator.validate(affectedTables, authorizationLevel)) {
const syntaxError = new SyntaxError("system tables are forbidden");
syntaxError.http_status = 403;
throw(syntaxError);

View File

@ -1,7 +1,7 @@
const AuthApi = require('../auth/auth_api');
const basicAuth = require('basic-auth');
module.exports = function authorization (metadataBackend, forceToBeAuthenticated = false) {
module.exports = function authorization (metadataBackend, forceToBeMaster = false) {
return function authorizationMiddleware (req, res, next) {
const { user } = res.locals;
const credentials = getCredentialsFromRequest(req);
@ -19,7 +19,7 @@ module.exports = function authorization (metadataBackend, forceToBeAuthenticated
const params = Object.assign({ metadataBackend }, res.locals, req.query, req.body);
const authApi = new AuthApi(req, params);
authApi.verifyCredentials(function (err, authenticated) {
authApi.verifyCredentials(function (err, authorizationLevel) {
if (req.profiler) {
req.profiler.done('authorization');
}
@ -28,9 +28,9 @@ module.exports = function authorization (metadataBackend, forceToBeAuthenticated
return next(err);
}
res.locals.authenticated = authenticated;
res.locals.authorizationLevel = authorizationLevel;
if (forceToBeAuthenticated && !authenticated) {
if (forceToBeMaster && authorizationLevel !== 'master') {
return next(new Error('permission denied'));
}

View File

@ -1,8 +1,8 @@
module.exports = function connectionParams (userDatabaseService) {
return function connectionParamsMiddleware (req, res, next) {
const { user, api_key: apikeyToken, authenticated } = res.locals;
const { user, api_key: apikeyToken, authorizationLevel } = res.locals;
userDatabaseService.getConnectionParams(user, apikeyToken, authenticated,
userDatabaseService.getConnectionParams(user, apikeyToken, authorizationLevel,
function (err, userDbParams, authDbParams) {
if (req.profiler) {
req.profiler.done('getConnectionParams');

View File

@ -1,6 +1,6 @@
module.exports = function timeoutLimits (metadataBackend) {
return function timeoutLimitsMiddleware (req, res, next) {
const { user, authenticated } = res.locals;
const { user, authorizationLevel } = res.locals;
metadataBackend.getUserTimeoutRenderLimits(user, function (err, timeoutRenderLimit) {
if (req.profiler) {
@ -12,7 +12,7 @@ module.exports = function timeoutLimits (metadataBackend) {
}
const userLimits = {
timeout: authenticated ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic
timeout: (authorizationLevel === 'master') ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic
};
res.locals.userLimits = userLimits;

View File

@ -1,10 +1,36 @@
const CdbRequest = require('../models/cartodb_request');
module.exports = function user () {
module.exports = function user(metadataBackend) {
const cdbRequest = new CdbRequest();
return function userMiddleware (req, res, next) {
res.locals.user = cdbRequest.userByReq(req);
next();
res.locals.user = getUserNameFromRequest(req, cdbRequest);
checkUserExists(metadataBackend, res.locals.user, function(err, userExists) {
if (err || !userExists) {
const error = new Error('Unauthorized');
error.type = 'auth';
error.subtype = 'user-not-found';
error.http_status = 404;
error.message = errorUserNotFoundMessageTemplate(res.locals.user);
next(error);
}
return next();
});
};
};
function getUserNameFromRequest(req, cdbRequest) {
return cdbRequest.userByReq(req);
}
function checkUserExists(metadataBackend, userName, callback) {
metadataBackend.getUserId(userName, function(err) {
callback(err, !err);
});
}
function errorUserNotFoundMessageTemplate(user) {
return `Sorry, we can't find CARTO user '${user}'. Please check that you have entered the correct domain.`;
}

View File

@ -30,8 +30,6 @@ var JobBackend = require('../batch/job_backend');
var JobCanceller = require('../batch/job_canceller');
var JobService = require('../batch/job_service');
var UserDatabaseMetadataService = require('../batch/user_database_metadata_service');
var cors = require('./middlewares/cors');
var GenericController = require('./controllers/generic_controller');
@ -160,8 +158,7 @@ function App(statsClient) {
var jobPublisher = new JobPublisher(redisPool);
var jobQueue = new JobQueue(metadataBackend, jobPublisher);
var jobBackend = new JobBackend(metadataBackend, jobQueue);
var userDatabaseMetadataService = new UserDatabaseMetadataService(metadataBackend);
var jobCanceller = new JobCanceller(userDatabaseMetadataService);
var jobCanceller = new JobCanceller();
var jobService = new JobService(jobBackend, jobCanceller);
var genericController = new GenericController();

View File

@ -15,7 +15,7 @@ const FORBIDDEN_ENTITIES = {
};
const Validator = {
validate(affectedTables, authenticated) {
validate(affectedTables, authorizationLevel) {
let hardValidationResult = true;
let softValidationResult = true;
@ -24,7 +24,7 @@ const Validator = {
hardValidationResult = this.hardValidation(affectedTables.tables);
}
if (!authenticated) {
if (authorizationLevel !== 'master') {
softValidationResult = this.softValidation(affectedTables.tables);
}
}

View File

@ -1,5 +1,3 @@
const _ = require('underscore');
function isApiKeyFound(apikey) {
return apikey.type !== null &&
apikey.user !== null &&
@ -15,6 +13,10 @@ function errorUserNotFoundMessageTemplate (user) {
return `Sorry, we can't find CARTO user '${user}'. Please check that you have entered the correct domain.`;
}
function isOauthAuthorization({ apikeyToken, authorizationLevel }) {
return (authorizationLevel === 'master') && !apikeyToken;
}
/**
* Callback is invoked with `dbParams` and `authDbParams`.
* `dbParams` depends on AuthApi verification so it might return a public user with just SELECT permission, where
@ -25,7 +27,7 @@ function errorUserNotFoundMessageTemplate (user) {
* @param {String} cdbUsername
* @param {Function} callback (err, dbParams, authDbParams)
*/
UserDatabaseService.prototype.getConnectionParams = function (username, apikeyToken, authenticated, callback) {
UserDatabaseService.prototype.getConnectionParams = function (username, apikeyToken, authorizationLevel, callback) {
this.metadataBackend.getAllUserDBParams(username, (err, dbParams) => {
if (err) {
err.http_status = 404;
@ -34,37 +36,14 @@ UserDatabaseService.prototype.getConnectionParams = function (username, apikeyTo
return callback(err);
}
const dbopts = {
const commonDBConfiguration = {
port: global.settings.db_port,
pass: global.settings.db_pubuser_pass
host: dbParams.dbhost,
dbname: dbParams.dbname,
};
dbopts.host = dbParams.dbhost;
dbopts.dbname = dbParams.dbname;
dbopts.user = (!!dbParams.dbpublicuser) ? dbParams.dbpublicuser : global.settings.db_pubuser;
const user = _.template(global.settings.db_user, {user_id: dbParams.dbuser});
let 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 (authenticated) {
dbopts.user = user;
dbopts.pass = pass;
}
let authDbOpts = _.defaults({ user: user, pass: pass }, dbopts);
if (!apikeyToken) {
return callback(null, dbopts, authDbOpts);
}
this.metadataBackend.getApikey(username, apikeyToken, (err, apikey) => {
this.metadataBackend.getMasterApikey(username, (err, masterApikey) => {
if (err) {
err.http_status = 404;
err.message = errorUserNotFoundMessageTemplate(username);
@ -72,16 +51,53 @@ UserDatabaseService.prototype.getConnectionParams = function (username, apikeyTo
return callback(err);
}
if (!isApiKeyFound(apikey)) {
return callback(null, dbopts, authDbOpts);
if (!isApiKeyFound(masterApikey)) {
const apiKeyNotFoundError = new Error('Unauthorized');
apiKeyNotFoundError.type = 'auth';
apiKeyNotFoundError.subtype = 'api-key-not-found';
apiKeyNotFoundError.http_status = 401;
return callback(apiKeyNotFoundError);
}
dbopts.user = apikey.databaseRole;
dbopts.pass = apikey.databasePassword;
const masterDBConfiguration = Object.assign({
user: masterApikey.databaseRole,
pass: masterApikey.databasePassword
},
commonDBConfiguration);
authDbOpts = _.defaults({ user: user, pass: pass }, dbopts);
if (isOauthAuthorization({ apikeyToken, authorizationLevel})) {
callback(null, masterDBConfiguration, masterDBConfiguration);
}
callback(null, dbopts, authDbOpts);
// Default Api key fallback
apikeyToken = apikeyToken || 'default_public';
this.metadataBackend.getApikey(username, apikeyToken, (err, apikey) => {
if (err) {
err.http_status = 404;
err.message = errorUserNotFoundMessageTemplate(username);
return callback(err);
}
if (!isApiKeyFound(apikey)) {
const apiKeyNotFoundError = new Error('Unauthorized');
apiKeyNotFoundError.type = 'auth';
apiKeyNotFoundError.subtype = 'api-key-not-found';
apiKeyNotFoundError.http_status = 401;
return callback(apiKeyNotFoundError);
}
const DBConfiguration = Object.assign({
user: apikey.databaseRole,
pass: apikey.databasePassword
},
commonDBConfiguration);
callback(null, DBConfiguration, masterDBConfiguration);
});
});
});
};

View File

@ -21,7 +21,7 @@ module.exports = function batchFactory (metadataBackend, redisPool, name, statsd
var jobQueue = new JobQueue(metadataBackend, jobPublisher);
var jobBackend = new JobBackend(metadataBackend, jobQueue);
var queryRunner = new QueryRunner(userDatabaseMetadataService);
var jobCanceller = new JobCanceller(userDatabaseMetadataService);
var jobCanceller = new JobCanceller();
var jobService = new JobService(jobBackend, jobCanceller);
var jobRunner = new JobRunner(jobService, jobQueue, queryRunner, metadataBackend, statsdClient);
var logger = new BatchLogger(loggerPath);

View File

@ -2,24 +2,26 @@
var PSQL = require('cartodb-psql');
function JobCanceller(userDatabaseMetadataService) {
this.userDatabaseMetadataService = userDatabaseMetadataService;
function JobCanceller() {
}
module.exports = JobCanceller;
JobCanceller.prototype.cancel = function (job, callback) {
this.userDatabaseMetadataService.getUserMetadata(job.data.user, function (err, userDatabaseMetadata) {
if (err) {
return callback(err);
}
doCancel(job.data.job_id, userDatabaseMetadata, callback);
});
const dbConfiguration = {
host: job.data.host,
port: job.data.port,
dbname: job.data.dbname,
user: job.data.dbuser,
pass: job.data.pass,
};
doCancel(job.data.job_id, dbConfiguration, callback);
};
function doCancel(job_id, userDatabaseMetadata, callback) {
var pg = new PSQL(userDatabaseMetadata);
function doCancel(job_id, dbConfiguration, callback) {
var pg = new PSQL(dbConfiguration);
getQueryPID(pg, job_id, function (err, pid) {
if (err) {

View File

@ -18,13 +18,9 @@ QueryRunner.prototype.run = function (job_id, sql, user, timeout, dbparams, call
return this._run(dbparams, job_id, sql, timeout, callback);
}
this.userDatabaseMetadataService.getUserMetadata(user, (err, userDBParams) => {
if (err) {
return callback(err);
}
const dbConfigurationError = new Error('Batch Job DB misconfiguration');
this._run(userDBParams, job_id, sql, timeout, callback);
});
return callback(dbConfigurationError);
};
QueryRunner.prototype._run = function (dbparams, job_id, sql, timeout, callback) {

View File

@ -1,7 +1,5 @@
'use strict';
var _ = require('underscore');
function UserDatabaseMetadataService(metadataBackend) {
this.metadataBackend = metadataBackend;
}
@ -23,20 +21,9 @@ UserDatabaseMetadataService.prototype.parseMetadataToDatabase = function (userDa
var dbopts = {};
dbopts.pass = dbParams.dbpass || global.settings.db_pubuser_pass;
dbopts.port = dbParams.dbport || global.settings.db_batch_port || global.settings.db_port;
dbopts.host = dbParams.dbhost;
dbopts.dbname = dbParams.dbname;
dbopts.user = (!!dbParams.dbpublicuser) ? dbParams.dbpublicuser : global.settings.db_pubuser;
// batch is secure so it's going to be authenticated by default
dbopts.authenticated = true;
dbopts.user = _.template(global.settings.db_user, { user_id: dbParams.dbuser });
dbopts.pass = _.template(global.settings.db_user_pass, {
user_id: dbParams.dbuser,
user_password: dbParams.dbpass
});
return dbopts;
};

View File

@ -23,7 +23,7 @@
"bunyan": "1.8.1",
"cartodb-psql": "0.12.0",
"cartodb-query-tables": "0.2.0",
"cartodb-redis": "1.0.0",
"cartodb-redis": "git://github.com/CartoDB/node-cartodb-redis.git#remove-auth-fallback",
"debug": "2.2.0",
"express": "~4.13.3",
"log4js": "cartodb/log4js-node#cdb",

View File

@ -6,6 +6,16 @@ var assert = require('../support/assert');
describe('app.auth', function() {
var scenarios = [
{
desc: 'no api key should fallback to default api key',
url: "/api/v1/sql?q=SELECT%20*%20FROM%20untitle_table_4",
statusCode: 200
},
{
desc: 'invalid api key should return 401',
url: "/api/v1/sql?api_key=THIS_API_KEY_NOT_EXIST&q=SELECT%20*%20FROM%20untitle_table_4",
statusCode: 401
},
{
desc: 'valid api key should allow insert in protected tables',
url: "/api/v1/sql?api_key=1234&q=INSERT%20INTO%20private_table%20(name)%20VALUES%20('app_auth_test1')",
@ -18,13 +28,8 @@ describe('app.auth', function() {
},
{
desc: 'invalid api key should NOT allow insert in protected tables',
url: "/api/v1/sql?api_key=RAMBO&q=INSERT%20INTO%20private_table%20(name)%20VALUES%20('RAMBO')",
statusCode: 403
},
{
desc: 'invalid api key (old redis location) should NOT allow insert in protected tables',
url: "/api/v1/sql?api_key=1235&q=INSERT%20INTO%20private_table%20(name)%20VALUES%20('RAMBO')",
statusCode: 403
url: "/api/v1/sql?api_key=THIS_API_KEY_NOT_EXIST&q=INSERT%20INTO%20private_table%20(name)%20VALUES%20('R')",
statusCode: 401
},
{
desc: 'no api key should NOT allow insert in protected tables',

View File

@ -18,33 +18,18 @@ describe('Auth API', function () {
});
});
// TODO: this is obviously a really dangerous sceneario, but in order to not break
// some uses cases (i.e: new carto.js examples) and keep backwards compatiblity we will keep it during some time.
// It should be fixed as soon as possible
it('should get result from query using a wrong API key', function (done) {
this.testClient = new TestClient({ apiKey: 'wrong' });
it('should fail when using a wrong API key', function (done) {
this.testClient = new TestClient({ apiKey: 'THIS_API_KEY_DOES_NOT_EXIST' });
this.testClient.getResult(publicSQL, (err, result) => {
assert.ifError(err);
assert.equal(result.length, 6);
done();
});
});
// TODO: this is obviously a really dangerous sceneario, but in order to not break
// some uses cases (i.e: new carto.js examples) and keep backwards compatiblity we will keep it during some time.
// It should be fixed as soon as possible
it('should fail while fetching data (private dataset) and using a wrong API key', function (done) {
this.testClient = new TestClient({ apiKey: 'wrong' });
const expectedResponse = {
response: {
status: 403
status: 401
}
};
this.testClient.getResult(privateSQL, expectedResponse, (err, result) => {
this.testClient.getResult(publicSQL, expectedResponse, (err, result) => {
assert.ifError(err);
assert.equal(result.error, 'permission denied for relation private_table');
assert.equal(result.error, 'Unauthorized');
done();
});
});
@ -106,63 +91,9 @@ describe('Auth API', function () {
});
});
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();
});
});
it('should fail while getting result from query using metadata and scoped dataset', function (done) {
this.testClient = new TestClient({ host: 'cartofante.cartodb.com' });
const expectedResponse = {
response: {
status: 403
},
anonymous: true
};
this.testClient.getResult(privateSQL, expectedResponse, (err, result) => {
assert.ifError(err);
assert.equal(result.error, 'permission denied for relation private_table');
done();
});
});
it('should insert and delete values on scoped datase using the master apikey', function (done) {
this.testClient = new TestClient({ apiKey: 4321, host: 'cartofante.cartodb.com' });
const insertSql = "INSERT INTO scoped_table_1(name) VALUES('wadus1')";
this.testClient.getResult(insertSql, (err, rows, body) => {
assert.ifError(err);
assert.ok(body.hasOwnProperty('time'));
assert.equal(body.total_rows, 1);
assert.equal(rows.length, 0);
const deleteSql = "DELETE FROM scoped_table_1 WHERE name = 'wadus1'";
this.testClient.getResult(deleteSql, (err, rows, body) => {
assert.ifError(err);
assert.ok(body.hasOwnProperty('time'));
assert.equal(body.total_rows, 1);
assert.equal(rows.length, 0);
done();
});
});
});
});
describe('Batch API', function () {
it('should create a job with regular api key and get it done', function (done) {
this.testClient = new BatchTestClient({ apiKey: 'regular1' });
it('should create a job with master api key and get it done', function (done) {
this.testClient = new BatchTestClient({ apiKey: '1234' });
this.testClient.createJob({ query: scopedSQL }, (err, jobResult) => {
if (err) {
@ -184,21 +115,42 @@ describe('Auth API', function () {
it('should create a job with regular api key and get it failed', function (done) {
this.testClient = new BatchTestClient({ apiKey: 'regular1' });
this.testClient.createJob({ query: privateSQL }, (err, jobResult) => {
this.testClient.createJob({ query: privateSQL }, { response: 403 }, (err, response) => {
if (err) {
return done(err);
}
jobResult.getStatus(function (err, job) {
if (err) {
return done(err);
}
const body = JSON.parse(response.body);
assert.equal(body.error, 'permission denied');
done();
});
});
assert.equal(job.status, JobStatus.FAILED);
assert.equal(job.failed_reason, 'permission denied for relation private_table');
it('should create a job with default public api key and get it failed', function (done) {
this.testClient = new BatchTestClient({ apiKey: 'default_public' });
done();
});
this.testClient.createJob({ query: publicSQL }, { response: 403 }, (err, response) => {
if (err) {
return done(err);
}
const body = JSON.parse(response.body);
assert.equal(body.error, 'permission denied');
done();
});
});
it('should create a job with fallback default public api key and get it failed', function (done) {
this.testClient = new BatchTestClient();
this.testClient.createJob({ query: publicSQL }, { response: 403, anonymous: true }, (err, response) => {
if (err) {
return done(err);
}
const body = JSON.parse(response.body);
assert.equal(body.error, 'permission denied');
done();
});
});
@ -267,77 +219,49 @@ describe('Auth API', function () {
});
});
// TODO: this is obviously a really dangerous sceneario, but in order to not break
// some uses cases (i.e: new carto.js examples) and to keep backwards compatiblity
// we will keep it during some time. It should be fixed as soon as possible
it('should get result from query using a wrong API key and quering to public dataset', function (done) {
this.testClient = new TestClient({ authorization: 'vizzuality:wrong' });
it('should fail when querying using a wrong API key', function (done) {
this.testClient = new TestClient({ authorization: 'vizzuality:THIS_API_KEY_DOES_NOT_EXIST' });
this.testClient.getResult(publicSQL, { anonymous: true }, (err, result) => {
assert.ifError(err);
assert.equal(result.length, 6);
done();
});
});
// TODO: this is obviously a really dangerous sceneario, but in order to not break
// some uses cases (i.e: new carto.js examples) and to keep backwards compatiblity
// we will keep it during some time. It should be fixed as soon as possible
it('should fail while fetching data (private dataset) and using a wrong API key', function (done) {
this.testClient = new TestClient({ authorization: 'vizzuality:wrong' });
const expectedResponse = {
response: {
status: 403
status: 401
},
anonymous: true
};
this.testClient.getResult(privateSQL, expectedResponse, (err, result) => {
this.testClient.getResult(publicSQL, expectedResponse, (err, result) => {
assert.ifError(err);
assert.equal(result.error, 'permission denied for relation private_table');
assert.equal(result.error, 'Unauthorized');
done();
});
});
describe('Batch API', function () {
it('should create a job with regular api key and get it done', function (done) {
this.testClient = new BatchTestClient({ authorization: 'vizzuality:regular1' });
it('should create a job with regular api key and get it failed', function (done) {
this.testClient = new BatchTestClient({ authorization: 'vizzuality:regular1', response: 403 });
this.testClient.createJob({ query: scopedSQL }, { anonymous: true }, (err, jobResult) => {
this.testClient.createJob({ query: scopedSQL }, { anonymous: true }, (err, response) => {
if (err) {
return done(err);
}
jobResult.getStatus(function (err, job) {
if (err) {
return done(err);
}
assert.equal(job.status, JobStatus.DONE);
done();
});
const body = JSON.parse(response.body);
assert.equal(body.error, 'permission denied');
done();
});
});
it('should create a job with regular api key and get it failed', function (done) {
this.testClient = new BatchTestClient({ authorization: 'vizzuality:regular1' });
it('should create a job with default api key and get it failed', function (done) {
this.testClient = new BatchTestClient({ authorization: 'vizzuality:default_public', response: 403 });
this.testClient.createJob({ query: privateSQL }, { anonymous: true }, (err, jobResult) => {
this.testClient.createJob({ query: privateSQL }, { anonymous: true }, (err, response) => {
if (err) {
return done(err);
}
jobResult.getStatus(function (err, job) {
if (err) {
return done(err);
}
assert.equal(job.status, JobStatus.FAILED);
assert.equal(job.failed_reason, 'permission denied for relation private_table');
done();
});
const body = JSON.parse(response.body);
assert.equal(body.error, 'permission denied');
done();
});
});

View File

@ -7,7 +7,6 @@ var JobPublisher = require('../../../batch/pubsub/job-publisher');
var JobQueue = require('../../../batch/job_queue');
var JobBackend = require('../../../batch/job_backend');
var JobService = require('../../../batch/job_service');
var UserDatabaseMetadataService = require('../../../batch/user_database_metadata_service');
var JobCanceller = require('../../../batch/job_canceller');
var metadataBackend = require('cartodb-redis')({ pool: redisUtils.getPool() });
@ -18,8 +17,7 @@ describe('batch module', function() {
var jobPublisher = new JobPublisher(pool);
var jobQueue = new JobQueue(metadataBackend, jobPublisher);
var jobBackend = new JobBackend(metadataBackend, jobQueue);
var userDatabaseMetadataService = new UserDatabaseMetadataService(metadataBackend);
var jobCanceller = new JobCanceller(userDatabaseMetadataService);
var jobCanceller = new JobCanceller();
var jobService = new JobService(jobBackend, jobCanceller);
before(function (done) {
@ -37,7 +35,11 @@ describe('batch module', function() {
var data = {
user: username,
query: sql,
host: dbInstance
host: dbInstance,
dbname: 'cartodb_test_user_1_db',
dbuser: 'test_cartodb_user_1',
port: 5432,
pass: 'test_cartodb_user_1_pass',
};
jobService.create(data, function (err, job) {
@ -60,7 +62,6 @@ describe('batch module', function() {
if (err) {
done(err);
}
assert.equal(job.status, 'running');
self.batch.drain(function () {

View File

@ -79,7 +79,7 @@ describe('job module', function() {
});
});
it('POST /api/v2/sql/job with wrong api key should respond with 403 permission denied', function (done){
it('POST /api/v2/sql/job with wrong api key should respond with 401 permission denied', function (done){
assert.response(server, {
url: '/api/v2/sql/job?api_key=wrong',
headers: { 'host': 'vizzuality.cartodb.com', 'Content-Type': 'application/x-www-form-urlencoded' },
@ -88,10 +88,10 @@ describe('job module', function() {
query: "SELECT * FROM untitle_table_4"
})
}, {
status: 403
status: 401
}, function(err, res) {
var error = JSON.parse(res.body);
assert.deepEqual(error, { error: [ 'permission denied' ] });
assert.deepEqual(error, { error: [ 'Unauthorized' ] });
done();
});
});
@ -134,16 +134,16 @@ describe('job module', function() {
});
});
it('GET /api/v2/sql/job/:job_id with wrong api key should respond with 403 permission denied', function (done){
it('GET /api/v2/sql/job/:job_id with wrong api key should respond with 401 permission denied', function (done){
assert.response(server, {
url: '/api/v2/sql/job/' + job.job_id + '?api_key=wrong',
headers: { 'host': 'vizzuality.cartodb.com', 'Content-Type': 'application/x-www-form-urlencoded' },
method: 'GET'
}, {
status: 403
status: 401
}, function(err, res) {
var error = JSON.parse(res.body);
assert.deepEqual(error, { error: [ 'permission denied' ] });
assert.deepEqual(error, { error: ['Unauthorized'] });
done();
});
});
@ -182,16 +182,16 @@ describe('job module', function() {
});
});
it('DELETE /api/v2/sql/job/:job_id with wrong api key should respond with 403 permission denied', function (done){
it('DELETE /api/v2/sql/job/:job_id with wrong api key should respond with 401 permission denied', function (done){
assert.response(server, {
url: '/api/v2/sql/job/' + job.job_id + '?api_key=wrong',
headers: { 'host': 'vizzuality.cartodb.com', 'Content-Type': 'application/x-www-form-urlencoded' },
method: 'DELETE'
}, {
status: 403
status: 401
}, function(err, res) {
var error = JSON.parse(res.body);
assert.deepEqual(error, { error: [ 'permission denied' ] });
assert.deepEqual(error, { error: ['Unauthorized'] });
done();
});
});

View File

@ -74,7 +74,7 @@ describe('query-tables-api', function() {
});
it('should skip cache to retrieve affected tables', function(done) {
var authenticatedRequest = {
var masterRequest = {
url: '/api/v1/sql?' + qs.stringify({
q: 'SELECT * FROM untitle_table_4',
api_key: '1234'
@ -84,7 +84,7 @@ describe('query-tables-api', function() {
},
method: 'GET'
};
assert.response(server, authenticatedRequest, RESPONSE_OK, function(err) {
assert.response(server, masterRequest, RESPONSE_OK, function(err) {
assert.ok(!err, err);
getCacheStatus(function(err, cacheStatus) {

View File

@ -10,7 +10,6 @@ var JobQueue = require('../../../batch/job_queue');
var JobBackend = require('../../../batch/job_backend');
var JobService = require('../../../batch/job_service');
var UserDatabaseMetadataService = require('../../../batch/user_database_metadata_service');
var JobCanceller = require('../../../batch/job_canceller');
var metadataBackend = require('cartodb-redis')({ pool: redisUtils.getPool() });
@ -19,8 +18,7 @@ describe('job queue', function () {
var jobPublisher = new JobPublisher(pool);
var jobQueue = new JobQueue(metadataBackend, jobPublisher);
var jobBackend = new JobBackend(metadataBackend, jobQueue);
var userDatabaseMetadataService = new UserDatabaseMetadataService(metadataBackend);
var jobCanceller = new JobCanceller(userDatabaseMetadataService);
var jobCanceller = new JobCanceller();
var jobService = new JobService(jobBackend, jobCanceller);
var userA = 'userA';

View File

@ -11,7 +11,6 @@ var JobQueue = require(BATCH_SOURCE + 'job_queue');
var JobBackend = require(BATCH_SOURCE + 'job_backend');
var JobPublisher = require(BATCH_SOURCE + 'pubsub/job-publisher');
var jobStatus = require(BATCH_SOURCE + 'job_status');
var UserDatabaseMetadataService = require(BATCH_SOURCE + 'user_database_metadata_service');
var JobCanceller = require(BATCH_SOURCE + 'job_canceller');
var PSQL = require('cartodb-psql');
@ -19,7 +18,6 @@ var metadataBackend = require('cartodb-redis')({ pool: redisUtils.getPool() });
var jobPublisher = new JobPublisher(redisUtils.getPool());
var jobQueue = new JobQueue(metadataBackend, jobPublisher);
var jobBackend = new JobBackend(metadataBackend, jobQueue);
var userDatabaseMetadataService = new UserDatabaseMetadataService(metadataBackend);
var JobFactory = require(BATCH_SOURCE + 'models/job_factory');
var USER = 'vizzuality';
@ -30,7 +28,6 @@ var HOST = 'localhost';
// in order to test query cancelation/draining
function runQueryHelper(job, callback) {
var job_id = job.job_id;
var user = job.user;
var sql = job.query;
job.status = jobStatus.RUNNING;
@ -40,22 +37,24 @@ function runQueryHelper(job, callback) {
return callback(err);
}
userDatabaseMetadataService.getUserMetadata(user, function (err, userDatabaseMetadata) {
const dbConfiguration = {
host: job.host,
port: job.port,
dbname: job.dbname,
user: job.dbuser,
pass: job.pass,
};
const pg = new PSQL(dbConfiguration);
sql = '/* ' + job_id + ' */ ' + sql;
pg.eventedQuery(sql, function (err, query) {
if (err) {
return callback(err);
}
var pg = new PSQL(userDatabaseMetadata);
sql = '/* ' + job_id + ' */ ' + sql;
pg.eventedQuery(sql, function (err, query) {
if (err) {
return callback(err);
}
callback(null, query);
});
callback(null, query);
});
});
}
@ -65,12 +64,16 @@ function createWadusJob(query) {
return JobFactory.create(JSON.parse(JSON.stringify({
user: USER,
query: query,
host: HOST
host: HOST,
dbname: 'cartodb_test_user_1_db',
dbuser: 'test_cartodb_user_1',
port: 5432,
pass: 'test_cartodb_user_1_pass',
})));
}
describe('job canceller', function() {
var jobCanceller = new JobCanceller(userDatabaseMetadataService);
var jobCanceller = new JobCanceller();
after(function (done) {
redisUtils.clean('batch:*', done);

View File

@ -23,7 +23,7 @@ var jobPublisher = new JobPublisher(redisUtils.getPool());
var jobQueue = new JobQueue(metadataBackend, jobPublisher);
var jobBackend = new JobBackend(metadataBackend, jobQueue);
var userDatabaseMetadataService = new UserDatabaseMetadataService(metadataBackend);
var jobCanceller = new JobCanceller(userDatabaseMetadataService);
var jobCanceller = new JobCanceller();
var jobService = new JobService(jobBackend, jobCanceller);
var queryRunner = new QueryRunner(userDatabaseMetadataService);
var StatsD = require('node-statsd').StatsD;
@ -35,7 +35,11 @@ var HOST = 'localhost';
var JOB = {
user: USER,
query: QUERY,
host: HOST
host: HOST,
dbname: 'cartodb_test_user_1_db',
dbuser: 'test_cartodb_user_1',
port: 5432,
pass: 'test_cartodb_user_1_pass',
};
describe('job runner', function() {

View File

@ -11,7 +11,6 @@ var JobQueue = require(BATCH_SOURCE + 'job_queue');
var JobBackend = require(BATCH_SOURCE + 'job_backend');
var JobPublisher = require(BATCH_SOURCE + 'pubsub/job-publisher');
var jobStatus = require(BATCH_SOURCE + 'job_status');
var UserDatabaseMetadataService = require(BATCH_SOURCE + 'user_database_metadata_service');
var JobCanceller = require(BATCH_SOURCE + 'job_canceller');
var JobService = require(BATCH_SOURCE + 'job_service');
var PSQL = require('cartodb-psql');
@ -20,8 +19,7 @@ var metadataBackend = require('cartodb-redis')({ pool: redisUtils.getPool() });
var jobPublisher = new JobPublisher(redisUtils.getPool());
var jobQueue = new JobQueue(metadataBackend, jobPublisher);
var jobBackend = new JobBackend(metadataBackend, jobQueue);
var userDatabaseMetadataService = new UserDatabaseMetadataService(metadataBackend);
var jobCanceller = new JobCanceller(userDatabaseMetadataService);
var jobCanceller = new JobCanceller();
var USER = 'vizzuality';
var QUERY = 'select pg_sleep(0)';
@ -29,7 +27,12 @@ var HOST = 'localhost';
var JOB = {
user: USER,
query: QUERY,
host: HOST
host: HOST,
dbname: 'cartodb_test_user_1_db',
dbuser: 'test_cartodb_user_1',
port: 5432,
pass: 'test_cartodb_user_1_pass',
};
function createWadusDataJob() {
@ -40,7 +43,6 @@ function createWadusDataJob() {
// in order to test query cancelation/draining
function runQueryHelper(job, callback) {
var job_id = job.job_id;
var user = job.user;
var sql = job.query;
job.status = jobStatus.RUNNING;
@ -50,22 +52,24 @@ function runQueryHelper(job, callback) {
return callback(err);
}
userDatabaseMetadataService.getUserMetadata(user, function (err, userDatabaseMetadata) {
const dbConfiguration = {
host: job.host,
port: job.port,
dbname: job.dbname,
user: job.dbuser,
pass: job.pass,
};
var pg = new PSQL(dbConfiguration);
sql = '/* ' + job_id + ' */ ' + sql;
pg.eventedQuery(sql, function (err, query) {
if (err) {
return callback(err);
}
var pg = new PSQL(userDatabaseMetadata);
sql = '/* ' + job_id + ' */ ' + sql;
pg.eventedQuery(sql, function (err, query) {
if (err) {
return callback(err);
}
callback(null, query);
});
callback(null, query);
});
});
}

View File

@ -188,7 +188,7 @@ HMSET api_keys:vizzuality:default_public \
user "vizzuality" \
type "default" \
grants_sql "true" \
database_role "test_windshaft_publicuser" \
database_role "testpublicuser" \
database_password "public"
EOF
@ -230,7 +230,7 @@ HMSET api_keys:cartodb250user:default_public \
user "cartodb250user" \
type "default" \
grants_sql "true" \
database_role "test_windshaft_publicuser" \
database_role "testpublicuser" \
database_password "public"
EOF

View File

@ -80,7 +80,12 @@ BatchTestClient.prototype.createJob = function(job, override, callback) {
if (err) {
return callback(err);
}
return callback(null, new JobResult(JSON.parse(res.body), this, override));
if (res.statusCode < 400) {
return callback(null, new JobResult(JSON.parse(res.body), this, override));
} else {
return callback(null, res);
}
}.bind(this)
);
};

View File

@ -99,7 +99,7 @@ it('can return user for verified signature', function(done){
oAuth.verifyRequest(req, metadataBackend, function(err, data){
assert.ok(!err, err);
assert.equal(data, 1);
assert.equal(data, 'master');
done();
});
});
@ -120,7 +120,7 @@ it('can return user for verified signature (for other allowed domains)', functio
oAuth.verifyRequest(req, metadataBackend, function(err, data){
oAuth.getAllowedHosts = oAuthGetAllowedHostsFn;
assert.ok(!err, err);
assert.equal(data, 1);
assert.equal(data, 'master');
done();
});
});

View File

@ -97,56 +97,56 @@ describe('pg entities access validator with validatePGEntitiesAccess enabled', f
});
it('validate function: should not be validated', function () {
let authenticated = true;
let authorizationLevel = 'master';
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCarto }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCarto }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCartodbKO }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCartodbKO }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPgcatalog }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPgcatalog }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesInfo }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesInfo }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPublicKO }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPublicKO }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesTopologyKO }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesTopologyKO }, authorizationLevel),
false
);
authenticated = false;
authorizationLevel = 'regular';
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCarto }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCarto }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCartodbKO }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesCartodbKO }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPgcatalog }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPgcatalog }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesInfo }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesInfo }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPublicKO }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesPublicKO }, authorizationLevel),
false
);
assert.strictEqual(
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesTopologyKO }, authenticated),
pgEntitiesAccessValidator.validate({ tables: fakeAffectedTablesTopologyKO }, authorizationLevel),
false
);
});