var crypto = require('crypto'); var Step = require('step'); var _ = require('underscore'); var debug = global.environment ? global.environment.debug : undefined; // Class handling map signatures and user certificates // // See https://github.com/CartoDB/Windshaft-cartodb/wiki/Signed-maps // // @param redis_pool an instance of a "redis-mpool" // See https://github.com/CartoDB/node-redis-mpool // Needs version 0.x.x of the API. // function SignedMaps(redis_pool) { this.redis_pool = redis_pool; // Database containing signatures // TODO: allow configuring ? // NOTE: currently it is the same as // the one containing layergroups this.db_signatures = 0; // // Map signatures in redis are reference to signature certificates // We have the following datastores: // // 1. User certificates: set of per-user authorization certificates // 2. Map signatures: set of per-map certificate references // 3. Certificate applications: set of per-certificate signed maps // User certificates (HASH:crt_id->crt_val) this.key_map_crt = "map_crt|<%= signer %>"; // Map signatures (SET:crt_id) this.key_map_sig = "map_sig|<%= signer %>|<%= map_id %>"; // Certificates applications (SET:map_id) // // Everytime a map is signed, the map identifier (layergroup_id) // is added to this set. The purpose of this set is to drop // all map signatures when a certificate is removed // this.key_crt_sig = "crt_sig|<%= signer %>|<%= crt_id %>"; }; var o = SignedMaps.prototype; //--------------- PRIVATE METHODS -------------------------------- o._acquireRedis = function(callback) { this.redis_pool.acquire(this.db_signatures, callback); }; o._releaseRedis = function(client) { this.redis_pool.release(this.db_signatures, client); }; /** * Internal function to communicate with redis * * @param redisFunc - the redis function to execute * @param redisArgs - the arguments for the redis function in an array * @param callback - function to pass results too. */ o._redisCmd = function(redisFunc, redisArgs, callback) { var redisClient; var that = this; var db = that.db_signatures; Step( function getRedisClient() { that.redis_pool.acquire(db, this); }, function executeQuery(err, data) { if ( err ) throw err; redisClient = data; redisArgs.push(this); redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs); }, function releaseRedisClient(err, data) { if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient); callback(err, data); } ); }; o._getAuthMethod = function(auth) { return auth.method || 'open'; }; //--------------- PUBLIC API ------------------------------------- /// Check formal validity of a certificate // /// Return an Error instance if invalid, null otherwise /// o.checkInvalidCertificate = function(cert) { //console.log("Checking cert: "); console.dir(cert); if ( cert.version !== "0.0.1" ) { return new Error("Unsupported certificate version " + cert.version); } if ( ! cert.auth ) { console.log("Cert is : "); console.dir(cert); return new Error("No certificate authorization"); } var method = this._getAuthMethod(cert.auth); switch ( method ) { case 'open': break; case 'token': if ( ! _.isArray(cert.auth.valid_tokens) ) return new Error("Invalid 'token' authentication: missing valid_tokens"); if ( ! cert.auth.valid_tokens.length ) return new Error("Invalid 'token' authentication: no valid_tokens"); break; default: return new Error("Unsupported authentication method: " + cert.auth.method); break; } return null; // all valid } // Check if the given certificate authorizes waiver of "auth" o.authorizedByCert = function(cert, auth) { auth = _.isArray(auth) ? auth : [auth]; var err = this.checkInvalidCertificate(cert); if ( err ) throw err; var method = this._getAuthMethod(cert.auth); // Open authentication certificates are always authorized if ( method === 'open' ) return true; // Token based authentication requires valid token if ( method === 'token' ) { return _.intersection(cert.auth.valid_tokens, auth).length > 0; } throw new Error("Unsupported authentication method: " + cert.auth.method); }; // Check if shown credential are authorized to access a map // by the given signer. // // @param signer a signer name (cartodb username) // @param map_id a layergroup_id // @param auth an authentication token, or undefined if none // (can still be authorized by signature) // // @param callback function(Error, Boolean) // o.isAuthorized = function(signer, map_id, auth, callback) { var that = this; var redisClient; var db = that.db_signatures; var authorized = false; var certificate_id_list; var missing_certificates = []; if ( debug ) { console.log("Check auth from signer '" + signer + "' on map '" + map_id + "' with auth '" + auth + "'"); } Step( function getRedisClient() { that.redis_pool.acquire(db, this); }, function getMapSignatures(err, client) { if ( err ) throw err; redisClient = client; var map_sig_key = _.template(that.key_map_sig, {signer:signer, map_id:map_id}); redisClient.SMEMBERS(map_sig_key, this); //that._redisCmd('SMEMBERS', [ map_sig_key ], this); }, function getCertificates(err, crt_lst) { if ( err ) throw err; if ( debug ) { console.log("Map '" + map_id + "' is signed by " + crt_lst.length + " certificates of user '" + signer); } certificate_id_list = crt_lst; if ( ! crt_lst.length ) { // No certs, avoid calling redis with short args list. // Next step expects a list of certificate values so // we directly send the empty list. return crt_lst; } var map_crt_key = _.template(that.key_map_crt, {signer:signer}); //that._redisCmd('HMGET', [ map_crt_key ].concat(crt_lst), this); redisClient.HMGET(map_crt_key, crt_lst, this); }, function checkCertificates(err, certs) { if ( err ) throw err; for (var i=0; i