first commit
This commit is contained in:
125
app/middlewares/authorization.js
Normal file
125
app/middlewares/authorization.js
Normal file
@@ -0,0 +1,125 @@
|
||||
'use strict';
|
||||
|
||||
const AuthApi = require('../auth/auth_api');
|
||||
const basicAuth = require('basic-auth');
|
||||
|
||||
module.exports = function authorization (metadataBackend, forceToBeMaster = false) {
|
||||
return function authorizationMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const credentials = getCredentialsFromRequest(req);
|
||||
|
||||
if (!userMatches(credentials, user)) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorization');
|
||||
}
|
||||
|
||||
return next(new Error('permission denied'));
|
||||
}
|
||||
|
||||
res.locals.api_key = credentials.apiKeyToken;
|
||||
|
||||
const params = Object.assign({ metadataBackend }, res.locals, req.query, req.body);
|
||||
const authApi = new AuthApi(req, params);
|
||||
|
||||
authApi.verifyCredentials(function (err, authorizationLevel) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorization');
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.authorizationLevel = authorizationLevel;
|
||||
|
||||
if (forceToBeMaster && authorizationLevel !== 'master') {
|
||||
return next(new Error('permission denied'));
|
||||
}
|
||||
|
||||
res.set('vary', 'Authorization'); //Honor Authorization header when caching.
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const credentialsGetters = [
|
||||
getCredentialsFromHeaderAuthorization,
|
||||
getCredentialsFromRequestQueryString,
|
||||
getCredentialsFromRequestBody,
|
||||
];
|
||||
|
||||
function getCredentialsFromRequest (req) {
|
||||
let credentials = null;
|
||||
|
||||
for (var getter of credentialsGetters) {
|
||||
credentials = getter(req);
|
||||
|
||||
if (apiKeyTokenFound(credentials)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return credentials;
|
||||
}
|
||||
|
||||
function getCredentialsFromHeaderAuthorization(req) {
|
||||
const { pass, name } = basicAuth(req) || {};
|
||||
|
||||
if (pass !== undefined && name !== undefined) {
|
||||
return {
|
||||
apiKeyToken: pass,
|
||||
user: name
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCredentialsFromRequestQueryString(req) {
|
||||
if (req.query.api_key) {
|
||||
return {
|
||||
apiKeyToken: req.query.api_key
|
||||
};
|
||||
}
|
||||
|
||||
if (req.query.map_key) {
|
||||
return {
|
||||
apiKeyToken: req.query.map_key
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCredentialsFromRequestBody(req) {
|
||||
if (req.body && req.body.api_key) {
|
||||
return {
|
||||
apiKeyToken: req.body.api_key
|
||||
};
|
||||
}
|
||||
|
||||
if (req.body && req.body.map_key) {
|
||||
return {
|
||||
apiKeyToken: req.body.map_key
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function apiKeyTokenFound(credentials) {
|
||||
if (typeof credentials === 'boolean') {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
if (credentials.apiKeyToken !== undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function userMatches (credentials, user) {
|
||||
return !(credentials.user !== undefined && credentials.user !== user);
|
||||
}
|
||||
146
app/middlewares/body-parser.js
Normal file
146
app/middlewares/body-parser.js
Normal file
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
|
||||
/*!
|
||||
* Connect - bodyParser
|
||||
* Copyright(c) 2010 Sencha Inc.
|
||||
* Copyright(c) 2011 TJ Holowaychuk
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var qs = require('qs');
|
||||
var multer = require('multer');
|
||||
|
||||
/**
|
||||
* Extract the mime type from the given request's
|
||||
* _Content-Type_ header.
|
||||
*
|
||||
* @param {IncomingMessage} req
|
||||
* @return {String}
|
||||
* @api private
|
||||
*/
|
||||
|
||||
function mime(req) {
|
||||
var str = req.headers['content-type'] || '';
|
||||
return str.split(';')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse request bodies.
|
||||
*
|
||||
* By default _application/json_, _application/x-www-form-urlencoded_,
|
||||
* and _multipart/form-data_ are supported, however you may map `connect.bodyParser.parse[contentType]`
|
||||
* to a function receiving `(req, options, callback)`.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* connect.createServer(
|
||||
* connect.bodyParser()
|
||||
* , function(req, res) {
|
||||
* res.end('viewing user ' + req.body.user.name);
|
||||
* }
|
||||
* );
|
||||
*
|
||||
* $ curl -d 'user[name]=tj' http://localhost/
|
||||
* $ curl -d '{"user":{"name":"tj"}}' -H "Content-Type: application/json" http://localhost/
|
||||
*
|
||||
* Multipart req.files:
|
||||
*
|
||||
* As a security measure files are stored in a separate object, stored
|
||||
* as `req.files`. This prevents attacks that may potentially alter
|
||||
* filenames, and depending on the application gain access to restricted files.
|
||||
*
|
||||
* Multipart configuration:
|
||||
*
|
||||
* The `options` passed are provided to each parser function.
|
||||
* The _multipart/form-data_ parser merges these with formidable's
|
||||
* IncomingForm object, allowing you to tweak the upload directory,
|
||||
* size limits, etc. For example you may wish to retain the file extension
|
||||
* and change the upload directory:
|
||||
*
|
||||
* server.use(bodyParser({ uploadDir: '/www/mysite.com/uploads' }));
|
||||
*
|
||||
* View [node-formidable](https://github.com/felixge/node-formidable) for more information.
|
||||
*
|
||||
* If you wish to use formidable directly within your app, and do not
|
||||
* desire this behaviour for multipart requests simply remove the
|
||||
* parser:
|
||||
*
|
||||
* delete connect.bodyParser.parse['multipart/form-data'];
|
||||
*
|
||||
* Or
|
||||
*
|
||||
* delete express.bodyParser.parse['multipart/form-data'];
|
||||
*
|
||||
* @param {Object} options
|
||||
* @return {Function}
|
||||
* @api public
|
||||
*/
|
||||
|
||||
exports = module.exports = function bodyParser(options){
|
||||
options = options || {};
|
||||
return function bodyParser(req, res, next) {
|
||||
if (req.body) {
|
||||
return next();
|
||||
}
|
||||
req.body = {};
|
||||
|
||||
if ('GET' === req.method || 'HEAD' === req.method) {
|
||||
return next();
|
||||
}
|
||||
var parser = exports.parse[mime(req)];
|
||||
if (parser) {
|
||||
parser(req, options, next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Parsers.
|
||||
*/
|
||||
|
||||
exports.parse = {};
|
||||
|
||||
/**
|
||||
* Parse application/x-www-form-urlencoded.
|
||||
*/
|
||||
|
||||
exports.parse['application/x-www-form-urlencoded'] = function(req, options, fn){
|
||||
var buf = '';
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', function(chunk){ buf += chunk; });
|
||||
req.on('end', function(){
|
||||
try {
|
||||
req.body = buf.length ? qs.parse(buf) : {};
|
||||
fn();
|
||||
} catch (err){
|
||||
fn(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse application/json.
|
||||
*/
|
||||
|
||||
exports.parse['application/json'] = function(req, options, fn){
|
||||
var buf = '';
|
||||
req.setEncoding('utf8');
|
||||
req.on('data', function(chunk){ buf += chunk; });
|
||||
req.on('end', function(){
|
||||
try {
|
||||
req.body = buf.length ? JSON.parse(buf) : {};
|
||||
fn();
|
||||
} catch (err){
|
||||
fn(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var multipartMiddleware = multer({ limits: { fieldSize: Infinity } });
|
||||
exports.parse['multipart/form-data'] = multipartMiddleware.none();
|
||||
23
app/middlewares/connection-params.js
Normal file
23
app/middlewares/connection-params.js
Normal file
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function connectionParams (userDatabaseService) {
|
||||
return function connectionParamsMiddleware (req, res, next) {
|
||||
const { user, api_key: apikeyToken, authorizationLevel } = res.locals;
|
||||
|
||||
userDatabaseService.getConnectionParams(user, apikeyToken, authorizationLevel,
|
||||
function (err, userDbParams, authDbParams) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('getConnectionParams');
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.userDbParams = userDbParams;
|
||||
res.locals.authDbParams = authDbParams;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
16
app/middlewares/cors.js
Normal file
16
app/middlewares/cors.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function cors(extraHeaders) {
|
||||
return function(req, res, next) {
|
||||
var baseHeaders = 'X-Requested-With, X-Prototype-Version, X-CSRF-Token, Authorization';
|
||||
|
||||
if(extraHeaders) {
|
||||
baseHeaders += ', ' + extraHeaders;
|
||||
}
|
||||
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Headers', baseHeaders);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
26
app/middlewares/db-quota.js
Normal file
26
app/middlewares/db-quota.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const PSQL = require('cartodb-psql');
|
||||
|
||||
const remainingQuotaQuery = 'SELECT _CDB_UserQuotaInBytes() - CDB_UserDataSize(current_schema()) AS remaining_quota';
|
||||
|
||||
module.exports = function dbQuota () {
|
||||
return function dbQuotaMiddleware (req, res, next) {
|
||||
const { userDbParams } = res.locals;
|
||||
const pg = new PSQL(userDbParams);
|
||||
pg.connect((err, client, done) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
client.query(remainingQuotaQuery, (err, result) => {
|
||||
if(err) {
|
||||
return next(err);
|
||||
}
|
||||
const remainingQuota = result.rows[0].remaining_quota;
|
||||
res.locals.dbRemainingQuota = remainingQuota;
|
||||
done();
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
91
app/middlewares/error.js
Normal file
91
app/middlewares/error.js
Normal file
@@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
|
||||
const errorHandlerFactory = require('../services/error_handler_factory');
|
||||
const MAX_ERROR_STRING_LENGTH = 1024;
|
||||
|
||||
module.exports = function error() {
|
||||
return function errorMiddleware(err, req, res, next) {
|
||||
const errorHandler = errorHandlerFactory(err);
|
||||
let errorResponse = errorHandler.getResponse();
|
||||
|
||||
if (global.settings.environment === 'development') {
|
||||
errorResponse.stack = err.stack;
|
||||
}
|
||||
|
||||
if (global.settings.environment !== 'test') {
|
||||
// TODO: email this Exception report
|
||||
console.error("EXCEPTION REPORT: " + err.stack);
|
||||
}
|
||||
|
||||
// Force inline content disposition
|
||||
res.header("Content-Disposition", 'inline');
|
||||
|
||||
if (req && req.profiler) {
|
||||
req.profiler.done('finish');
|
||||
res.header('X-SQLAPI-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
|
||||
setErrorHeader(errorHandler, res);
|
||||
|
||||
res.header('Content-Type', 'application/json; charset=utf-8');
|
||||
res.status(getStatusError(errorHandler, req));
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(errorResponse);
|
||||
} else {
|
||||
res.json(errorResponse);
|
||||
}
|
||||
|
||||
if (req && req.profiler) {
|
||||
res.req.profiler.sendStats();
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function getStatusError(errorHandler, req) {
|
||||
let statusError = errorHandler.http_status;
|
||||
|
||||
// JSONP has to return 200 status error
|
||||
if (req && req.query && req.query.callback) {
|
||||
statusError = 200;
|
||||
}
|
||||
|
||||
return statusError;
|
||||
}
|
||||
|
||||
function setErrorHeader(errorHandler, res) {
|
||||
const errorsLog = {
|
||||
context: errorHandler.context,
|
||||
detail: errorHandler.detail,
|
||||
hint: errorHandler.hint,
|
||||
statusCode: errorHandler.http_status,
|
||||
message: errorHandler.message
|
||||
};
|
||||
|
||||
res.set('X-SQLAPI-Errors', stringifyForLogs(errorsLog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove problematic nested characters
|
||||
* from object for logs RegEx
|
||||
*
|
||||
* @param {Object} object
|
||||
*/
|
||||
function stringifyForLogs(object) {
|
||||
Object.keys(object).map(key => {
|
||||
if (typeof object[key] === 'string') {
|
||||
object[key] = object[key]
|
||||
.substring(0, MAX_ERROR_STRING_LENGTH)
|
||||
.replace(/[^a-zA-Z0-9]/g, ' ');
|
||||
} else if (typeof object[key] === 'object') {
|
||||
stringifyForLogs(object[key]);
|
||||
} else if (object[key] instanceof Array) {
|
||||
for (let element of object[key]) {
|
||||
stringifyForLogs(element);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.stringify(object);
|
||||
}
|
||||
24
app/middlewares/profiler.js
Normal file
24
app/middlewares/profiler.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
module.exports.initializeProfilerMiddleware = function initializeProfiler (label) {
|
||||
return function initializeProfilerMiddleware (req, res, next) {
|
||||
if (req.profiler) {
|
||||
req.profiler.start(`sqlapi.${label}`);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.finishProfilerMiddleware = function finishProfiler () {
|
||||
return function finishProfilerMiddleware (req, res, next) {
|
||||
if (req.profiler) {
|
||||
req.profiler.end();
|
||||
req.profiler.sendStats();
|
||||
|
||||
res.header('X-SQLAPI-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
61
app/middlewares/rate-limit.js
Normal file
61
app/middlewares/rate-limit.js
Normal file
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const RATE_LIMIT_ENDPOINTS_GROUPS = {
|
||||
QUERY: 'query',
|
||||
JOB_CREATE: 'job_create',
|
||||
JOB_GET: 'job_get',
|
||||
JOB_DELETE: 'job_delete',
|
||||
COPY_FROM: 'copy_from',
|
||||
COPY_TO: 'copy_to'
|
||||
};
|
||||
|
||||
|
||||
function rateLimit(userLimits, endpointGroup = null) {
|
||||
if (!isRateLimitEnabled(endpointGroup)) {
|
||||
return function rateLimitDisabledMiddleware(req, res, next) { next(); };
|
||||
}
|
||||
|
||||
return function rateLimitMiddleware(req, res, next) {
|
||||
userLimits.getRateLimit(res.locals.user, endpointGroup, function(err, userRateLimit) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!userRateLimit) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [isBlocked, limit, remaining, retry, reset] = userRateLimit;
|
||||
|
||||
res.set({
|
||||
'Carto-Rate-Limit-Limit': limit,
|
||||
'Carto-Rate-Limit-Remaining': remaining,
|
||||
'Carto-Rate-Limit-Reset': reset
|
||||
});
|
||||
|
||||
if (isBlocked) {
|
||||
// retry is floor rounded in seconds by redis-cell
|
||||
res.set('Retry-After', retry + 1);
|
||||
|
||||
const rateLimitError = new Error(
|
||||
'You are over platform\'s limits. Please contact us to know more details'
|
||||
);
|
||||
rateLimitError.http_status = 429;
|
||||
rateLimitError.context = 'limit';
|
||||
rateLimitError.detail = 'rate-limit';
|
||||
return next(rateLimitError);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function isRateLimitEnabled(endpointGroup) {
|
||||
return global.settings.ratelimits.rateLimitsEnabled &&
|
||||
endpointGroup &&
|
||||
global.settings.ratelimits.endpoints[endpointGroup];
|
||||
}
|
||||
|
||||
module.exports = rateLimit;
|
||||
module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS;
|
||||
25
app/middlewares/timeout-limits.js
Normal file
25
app/middlewares/timeout-limits.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function timeoutLimits (metadataBackend) {
|
||||
return function timeoutLimitsMiddleware (req, res, next) {
|
||||
const { user, authorizationLevel } = res.locals;
|
||||
|
||||
metadataBackend.getUserTimeoutRenderLimits(user, function (err, timeoutRenderLimit) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('getUserTimeoutLimits');
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const userLimits = {
|
||||
timeout: (authorizationLevel === 'master') ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic
|
||||
};
|
||||
|
||||
res.locals.userLimits = userLimits;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
38
app/middlewares/user.js
Normal file
38
app/middlewares/user.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const CdbRequest = require('../models/cartodb_request');
|
||||
|
||||
module.exports = function user(metadataBackend) {
|
||||
const cdbRequest = new CdbRequest();
|
||||
|
||||
return function userMiddleware (req, res, 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.`;
|
||||
}
|
||||
Reference in New Issue
Block a user