UNPKG

node-hdb-pool

Version:

SAP HANA connection pool

732 lines (599 loc) 22.1 kB
/** * environment/named pool manager for HANA */ /** * HDB environment pools * * Environment pool is for HDB operations using technical user credentials stored in the configuration file * * general properties: * * only one pool is created per environment * * pool is connecting with the static username and password stored in the configuration file * * max pool size per environment can be defined in the conf file * * data structure: * * envPools stores a single HDBPool object per environment (e.g. envPools['HDB']) * * cleanup: * * idle connections are closed after POOL_TTL_MS milliseconds of inactivity * */ /** * HDB user pools * * Named pool is for HDB operations using effective SAML identity of the authenticated user * * general properties: * * one user pool is created per user per environment * * user pools are initialized on-demand * * pool is connecting first with SAML assertion, then with HANA sessionCookie * * max size of the user pool is set by Conf.userpool.POOL_SIZE * * min pool size should be 1 to keep at least one connection alive in order to keep HANA sessionCookie alive * * data structure: * * userPools are keyed by userId * * for each userId: * * created date: pool registration timestamp * * pools: HDBPool objects for every target environment which was visited by the user * * sessions: references to express sessions in order to handle multiple express sessions (e.g. access from different devices) per user * * lastAccess: last time the user initiated a request * * cleanup: * * if lastAccess of a given user is more than Conf.userpool.USER_TIMEOUT seconds ago: * * all registered express sessions will be destroyed (kelas: DISABLED - this is an overkill) * * user pools on all environments will be drained * * user is evicted from userPools object * */ 'use strict'; var hdbPool = require('./hdbPool.js'), WSS = require('hana-saml-wsse'), precise = require('precise'), moment = require('moment'), url = require('url'), async = require('async'), hdbPoolLogger = global.logger || require('tracer').colorConsole({stackIndex: 1}); var SYSFOOTPRINT = {}; // store system information like SID, HOST, VERSION var QUERYMODE = { EXEC: 'exec', META: 'meta', STREAM: 'stream' }; var userPools = {}, envPools = {}; var wssclient; var SESSION_EXPIRED_TOKEN = 'SESSION_EXPIRED::'; var Conf = { usernamePrefix: '', sourceSysField: 'source-sys', samlDelegation: { loglevel: 'info' }, userpool: { CLEANUP_INTERVAL: 0, POOL_SIZE: 3, POOL_TTL_MS: 30000, CLIENT_PID: 777, USER_TIMEOUT: 10, POOL_LOG: null }, http: { HEADER_PREFIX: 'x-hdb-' }, db: { default: 'HDB', HDB: { HOST: 'hdbhost', PORT: 30015, USER: 'USER', PASS: '****', DEFAULT_SCHEMA: 'USER', POOL_SIZE: 3, POOL_TTL_MS: 30000, POOL_LOG: null } } }; function configure(c) { // TODO config sanity check Conf = c; } // initialise environment pools function initPools(cb) { if (Conf.userpool.CLIENT_PID) hdbPool.setPid(Conf.userpool.CLIENT_PID); // fake pid to pass to HANA in order to retain sessionCookie between restarts) // initialization of user- and environment-pools are independent, and can happen parallel async.parallel([ function(callback) { initEnvPools(function(err) { if (err) { logger.error('error initializing environment pools:', err); } return callback(); }); }, function(callback) { initUserPools(callback); } ], function(err, results) { return cb && cb(); }); } // initialise user pools (SAML/sessionCookie-based authentication) function initUserPools(cb) { logger.info('initUserPools'); if (Conf.samlDelegation) { Conf.samlDelegation.log = logger[Conf.samlDelegation.loglevel]; wssclient = new WSS.client(Conf.samlDelegation); } userPools = {}; setInterval(cleanupUserPools, Conf.userpool.CLEANUP_INTERVAL * 1000); // setup periodic cleanup routine // user pools are initialized on-demand if (cb) return cb(); } // initialise environment pools (username+password auth) function initEnvPools(cb) { logger.info('initEnvPools'); envPools = {}; var envs = _.without(_.keys(Conf.db), 'default'); logger.debug('initializing pools', envs); async.each(envs, function(env, callback) { var c = Conf.db[env]; var poolOpts = { host: c.HOST, port: c.PORT, env: env, user: c.USER, password: c.PASS, logger: hdbPoolLogger, maxPoolSize: c.POOL_SIZE, idleTimeoutMillis: c.POOL_TTL_MS || 30000, // specifies how long a resource can stay idle in pool before being removed genericPoolLog: c.POOL_LOG, resphPrefix: Conf.http.HEADER_PREFIX || 'x-hdb-', defaultSchema: c.DEFAULT_SCHEMA || c.SCHEMA }; envPools[env] = hdbPool.createPool(poolOpts); // get system_id, host, and version envPools[env].exec('SELECT system_id SID, host, version FROM M_DATABASE', [], function(err, rows) { if (err || rows.length !== 1) { return callback('Error while getting system information on ' + env); } SYSFOOTPRINT[env] = rows[0]; logger.debug('System information updated on ' + env); return callback(); }); }, function(err) { if (cb) return cb(err); }); } // function to decide if the query needs to be executed on an environment pool or on a user pool function query(querymode, sql, args, req, streams, cb, type) { var env = getEnvFromRequest(req); if (!env || !env.match(/^[A-Za-z0-9_]+$/) || !Conf.db[env]) { var err = 'invalid ' + Conf.sourceSysField + ' value: ' + env; logger.error(err); return cb ? cb(err) : null; } // if we have a valid SAML session, execute the query on a user pool if (req && req.SAMLAuthenticated && req.session && req.session.passport && req.session.passport.user) return queryUserPool(querymode, env, sql, args, req, streams, cb, type); // execute the query on an env pool queryEnvPool(querymode, env, sql, args, req, streams, cb, type); } // try to connect function detectSAMLerror(env, userId, assertion, cb) { function flushPool(env) { var p = envPools[env].pool; logger.debug(env + ' initiating emergency pool flush...'); p.destroyAllNow(); } function unknownAuthError(env, userId) { logger.error(env + ':' + userId + ': unexpected error: assertion appears valid but direct SAML authentication earlier returned an error'); // the env pool is tainted - nuke all resources immediately flushPool(env); } assertion = assertion.replace("'", '', 'g'); // assertion content should be fairly safe but strip it anyway queryEnvPool('exec', env, "CONNECT WITH SAML ASSERTION '" + assertion + "'", [], function(err) { if (err) return cb(err); // check if user is deacticated queryEnvPool('exec', env, "SELECT USER_DEACTIVATED FROM SYS.USERS WHERE USER_NAME = '" + userId + "'", [], function(err2, rows) { if (err2) { unknownAuthError(env, userId); return cb(err2); } if (rows && rows.length == 1 && rows[0].USER_DEACTIVATED === 'TRUE') { logger.error(env + ':' + userId + ': unexpected error: user is deactivated in the HANA database'); flushPool(env); // formulate a special error message that can be used to notify the end-user return cb({'code': 10, 'deactivatedUser': true}); } unknownAuthError(env, userId); return cb(err); }); }); } function queryUserPool(querymode, env, sql, args, req, streams, cb, type) { logger.trace('queryUserPool'); createUserPool(req, env, function(err, userId, env) { if (err) return cb(err); var pool = userPools[userId].pools[env]; pool[querymode](sql, args, req, streams, function(err, result) { // detect 10: Authentication Failed HANA error var isAuthFailed = (err && (err.code === 10)); // detect the error case when user does not exist in the target environment but we have tried to connect with its username + saml assertion var isMissingUser = (err && err.code === 591 && err.message && err.message.indexOf('Invalid principal id for principal $principalName$') > 0); // no special handling required if (!isAuthFailed && !isMissingUser) return cb(err, result); // handle errors which can block resource creation in the pool, such as: // - HANA session timed out -> 10: authentication failed // - SAML assertion timed out -> 10: authentication failed // - invalid SAML assertion -> 10: authentication failed // - user is not yet created on HANA -> 591: internal error: Invalid principal id for principal $principalName$ var _drain = function(error) { delete error.assertion; // don't propagate failed assertion further upstream - potentially unsafe var p = pool.pool; p.min = 0; // prevent re-creating resources when min > 0 var _clean = function(error) { userPools[userId].pools[env] = null; // destroy the reference // clean the HANA session cookie if (req.session && req.session.passport && req.session.passport.user && req.session.passport.user[env] && req.session.passport.user[env].HANAsessionCookie) { req.session.passport.user[env].HANAsessionCookie = null; req.session.save(); } return cb(error, result); } if (p.getPoolSize() === 0) return _clean(error); // no need to drain empty pool, just clean it // drain and clean non-empty pool p.drain(function() { logger.warn(env + ':' + userId + ' expired pool destroyed'); p.destroyAllNow(); return _clean(error); }); } if (isMissingUser) { logger.error(userId + ': user is not created yet on ' + env); _drain(err); } else if (isAuthFailed) { if (pool._authMethod === 'assertion' && err.assertion) { // try to figure out what went wrong detectSAMLerror(env, userId, err.assertion, _drain); } else { // HANA session cookie expired logger.error(userId + ': expired HANA ' + pool._authMethod + ' on ' + env); _drain(err); } } }, type); }); } function queryEnvPool(querymode, env, sql, args, req, streams, cb, type) { logger.trace('queryEnvPool called'); envPools[env][querymode](sql, args, req, streams, cb, type); } function getAssertionFactory(masterAssertion) { var masterAssertionInfo = wssclient.getAssertionInfo(masterAssertion), expDate = masterAssertionInfo.notOnOrAfter; // check if delegatable assertion is still valid if (expDate && moment(expDate).isBefore(moment())) { throw new Error(SESSION_EXPIRED_TOKEN + 'master assertion no longer valid (expired ' + moment(expDate).fromNow() + ')'); } return function(cb) { logger.info('[wsa] requesting delegated assertion'); var timer = precise().start(); wssclient.get('wsa', function(err, assertion, assertionInfo) { if (err) { logger.error('[wsa]', err); } else { logger.info('[wsa] rcvd delegated assertion, roundrip', (timer.stop().diff()/1000000000).toFixed(2) + 's,', assertionInfo.notOnOrAfter ? ('expires ' + moment(assertionInfo.notOnOrAfter).fromNow()) : 'no expiration date'); } cb(err, assertion, assertionInfo); }, masterAssertion); } } function createUserPool(req, env, cb) { if (req && req.session && !req.session.id) { logger.error('empty session id'); return cb && cb('empty session id'); } logger.trace('createUserPool called'); var userId = getUserId(req); if (!userId) { var err = 'session is not authenticated'; logger.error(err, req.session); if (cb) return cb(err); return; } if (!env) { env = getEnvFromRequest(req); } if (!Conf.db[env]) { var err = 'Invalid ' + Conf.sourceSysField + ' value: ' + env; logger.error(err); if (cb) return cb(err); return; } if (!userPools[userId]) { // if user is not registered in the userPools yet logger.info('registering user ' + userId + ' in userPools'); var o = userPools[userId] = {}; o.created = new Date(); o.pools = {}; o.sessions = {}; // storing express sessions } //console.log(req.session); if (userPools[userId] && !userPools[userId].sessions[req.session.id]) { logger.info(req.session.id + ' express session registered in the userPools'); userPools[userId].sessions[req.session.id] = req.session; } if (userPools[userId]) userPools[userId].lastAccess = new Date(); if (userPools[userId].pools[env]) { return cb ? cb(null, userId, env) : null; // found existing pool, nothing to do } // user does not have a pool for the target environment if (!req || !req.session || !req.session.passport || !req.session.passport.user) { logger.error(userId + ' express session does not contain sufficient information for hdb user pool creation.'); return cb ? cb('unsupported hdb user pool authentication mode (session is not authenticated)') : null; } logger.info(userId, 'registering hdb user pool for ' + env); // common pool options var poolOpts = { host: Conf.db[env].HOST, port: Conf.db[env].PORT, env: env, logger: hdbPoolLogger, minPoolSize: 1, // we keep at least one connection alive to maintain HANA sessionCookie maxPoolSize: Conf.userpool.POOL_SIZE || 1, idleTimeoutMillis: Conf.userpool.POOL_TTL_MS || 30000, // specifies how long a resource can stay idle in pool before being removed genericPoolLog: Conf.db[env].POOL_LOG, refreshIdle: false, // keep alive at least _minPoolSize_ connections (if true, connections are destroyed and recreated every _idleTimeoutMillis_) resphPrefix: Conf.http.HEADER_PREFIX || 'x-hdb-', defaultSchema: Conf.db[env].DEFAULT_SCHEMA || Conf.db[env].SCHEMA }; var passportUser = req.session.passport.user, passportUserEnv = passportUser[env], poolAuthOpts = {}, _trc = env + ':' + userId + ':'; // sessionCookie takes precedence if present if (passportUserEnv && passportUserEnv.HANAUser && passportUserEnv.HANAsessionCookie) { poolAuthOpts = { method: 'sessionCookie', user: passportUserEnv.HANAUser, sessionCookie: passportUserEnv.HANAsessionCookie }; } else if (passportUser.assertion) { var assertionOrAssertionFactory; try { assertionOrAssertionFactory = wssclient ? getAssertionFactory(passportUser.assertion) : passportUser.assertion; } catch (e) { return cb(e); } poolAuthOpts = { method: 'assertion', user: userId, assertion: assertionOrAssertionFactory }; } else { logger.error(userId + ' neither assertion nor sessionCookie was found in the express session, unable to create user pool'); return cb ? cb('unsupported hdb user pool authentication mode') : null; } poolOpts = _.extend(poolOpts, poolAuthOpts); userPools[userId].pools[env] = hdbPool.createPool(poolOpts, req); // store auth type so that later on we can try to detect assertion-specific error userPools[userId].pools[env]._authMethod = poolAuthOpts.method; logger.info(_trc, 'user pool created using', poolAuthOpts.method, 'auth'); if (cb) return cb(null, userId, env); } // comma-separated values function csv(sql, args, req, outstream, cb) { logger.trace('csv called'); query(QUERYMODE.STREAM, sql, args, req, [hdbPool.createCsvStringifier, outstream], cb); } // semicolon-separated values function ssv(sql, args, req, outstream, cb) { logger.trace('ssv called'); query(QUERYMODE.STREAM, sql, args, req, [hdbPool.createSsvStringifier, outstream], cb); } function json(sql, args, req, outstream, cb) { logger.trace('json called'); query(QUERYMODE.STREAM, sql, args, req, [hdbPool.createJSONStringifier, outstream], cb); } function exec(sql, args, req, outstream, cb) { logger.trace('exec called'); query(QUERYMODE.EXEC, sql, args, req, outstream, cb); } function meta(sql, args, req, outstream, cb) { logger.trace('meta called'); query(QUERYMODE.META, sql, args, req, outstream, cb); } // check if Conf.sourceSysField is present or not function hasEnv(req) { // check header if (req.headers[Conf.sourceSysField]) return true; // check GET params if (req.query[Conf.sourceSysField]) return true; if (req.session && req.session.returnTo) { var parsed = url.parse(req.session.returnTo, true); if (parsed.query && parsed.query[Conf.sourceSysField]) return true; } return false; } function getEnvFromRequest(req) { // set default var env = Conf.db.default || 'HDB'; // check in session if (req && req.session && req.session[Conf.sourceSysField]) env = req.session[Conf.sourceSysField]; // check in header if (req && req.headers && req.headers[Conf.sourceSysField]) env = req.headers[Conf.sourceSysField]; // check in GET params if (req && req.query && req.query[Conf.sourceSysField]) env = req.query[Conf.sourceSysField]; else if (req && req.session && req.session.returnTo) { var parsed = url.parse(req.session.returnTo, true); if (parsed.query && parsed.query[Conf.sourceSysField]) env = parsed.query[Conf.sourceSysField]; } // check in POST params if (req && req.body && req.body[Conf.sourceSysField]) env = req.body[Conf.sourceSysField]; return env; } function getSystemFootprint(req) { var env = getEnvFromRequest(req), sysft = SYSFOOTPRINT[env] || {}; if (!sysft.SID) sysft.SID = 'NaN'; if (!sysft.HOST) sysft.HOST = 'NaN'; if (!sysft.VERSION) sysft.VERSION = 'NaN'; return sysft; } // checks if the user already has a pool in the given environment function hasUserPool(req) { var userId = getUserId(req), env = getEnvFromRequest(req); // check if the pool is already initialised and stored in userPools object if (userPools && userPools[userId] && userPools[userId].pools && userPools[userId].pools[env]) return true; // check if the sessionCookie is stored in the express session for the given environment if (req && req.session && req.session.passport && req.session.passport.user && req.session.passport.user[env] && req.session.passport.user[env].HANAsessionCookie) return true; return false; } function logout(req, next) { if (!req || !req.session || !req.session.passport || !req.session.passport.user) return next(); var sessionId = req.session.id, userId = getUserId(req); // kill express session req.session.destroy(function() { logger.info(userId + ': destroyed session ' + sessionId); var poolObj = userPools[userId]; if (!poolObj) { logger.info(userId + ': no hdb user pool to destroy'); return next(); } if (!poolObj.sessions) { logger.info(userId + ': no express sessions stored in meta pool'); // FIXME should be safe to call `drainUserPool` here? return next(); } // check if the user has any additional active Express sessions async.detect(_.keys(poolObj.sessions), function(sessId, cb) { req.sessionStore.get(sessId, function(err, session) { return err ? cb(false) : cb(session); // if session is not active, session will be `undefined` }); }, function(activeSession) { if (activeSession) { logger.trace(userId + ': has additional active express sessions, retaining hdb user pool'); return next(); } // destroy user pool logger.trace(userId + ': no more active express sessions, destroying hdb user pool'); return drainUserPool(userId, next); }); }); } // private methods // ------------------------------------------------------------------- function getUserId(req) { // get userId var userId = 'UNAUTHENTICATED'; // TODO handle unathenticated case var usernameSessionKey = Conf.usernameSessionKey || 'nameID'; if (req && req.session && req.session.passport && req.session.passport.user) { userId = req.session.passport.user[usernameSessionKey] || req.session.passport.user.nameID; } else { logger.error('no user id is found in the session (' + usernameSessionKey + ' = ?)'); return null; } return Conf.usernamePrefix + userId; } function cleanupUserPools() { var timeoutThreshold = new Date().getTime() - Conf.userpool.USER_TIMEOUT * 1000; _.each(userPools, function(o, userId) { if (o.lastAccess < timeoutThreshold) { drainUserPool(userId, function() { logger.info('all hdb pools for', userId, 'are drained'); }); // destroy all Express sessions of the given user, // e.g. if logged in from different browsers/devices async.each(_.values(o.sessions), function(session, callback) { logger.info(userId + ' ' + session.id + ' express session destroyed'); var envs = _.without(_.keys(Conf.db), 'default'); envs.forEach(function(env) { if (session.passport.user[env]) { logger.info('clearing HDB sessionCookie for', env + ':' + userId + ' -> ' + session.passport.user[env].HANAsessionCookie); delete session.passport.user[env]; } }); callback(); // overkill //session.destroy(callback); // // if (session.save) { session.save(); logger.debug('updated session'); } }, function(err) { if (err) logger.error(userId + ' error while destroying express sessions', err); //drainUserPool(userId); }); } }); } function drainUserPool(userId, cb) { if (!userPools[userId] && !userPools[userId].pools) { // unregister user from meta-pools delete userPools[userId]; logger.info(userId + ' does not have a registered hdb pool'); return cb ? cb() : null; } var pools = userPools[userId].pools; // unregister user from meta pools delete userPools[userId]; // drain all pools async.forEachOf(pools, function(pool, env, callback) { if (!pool || !pool.pool) // pool is already killed because e.g. session timeout was detected return callback(); pool.pool.min = 0; // prevent re-creating resources when min > 0 pool.pool.drain(function() { logger.info(userId + ' pool destroyed on ' + env); pool.pool.destroyAllNow(); return callback(); }); }, function(err) { if (cb) return cb(); }); } exports.configure = configure; exports.initPools = initPools; exports.createUserPool = createUserPool; exports.hasEnv = hasEnv; exports.hasUserPool = hasUserPool; exports.getEnv = getEnvFromRequest; exports.csv = csv; exports.ssv = ssv; exports.json = json; exports.exec = exec; exports.meta = meta; exports.getSystemFootprint = getSystemFootprint; exports.getAssertionFactory = getAssertionFactory; exports.q = queryEnvPool; exports.logout = logout;