From 45e6241e34469565d22c727c1df6fd8f386386d9 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 12 Nov 2014 23:52:45 -0300 Subject: [PATCH 1/2] feature: remove profile and remove key --- config/routes.js | 3 + plugins/emailstore.js | 155 ++++++++++++++++++++++++++++++++-------- test/test.EmailStore.js | 53 ++++++++++++++ 3 files changed, 180 insertions(+), 31 deletions(-) diff --git a/config/routes.js b/config/routes.js index d4ed474d..1ad874c8 100644 --- a/config/routes.js +++ b/config/routes.js @@ -63,6 +63,9 @@ module.exports = function(app) { app.post(apiPrefix + '/email/register', emailPlugin.oldSave); app.get(apiPrefix + '/email/retrieve/:email', emailPlugin.oldRetrieve); + + app.post(apiPrefix + '/email/delete/profile', emailPlugin.eraseProfile); + app.post(apiPrefix + '/email/delete/item/:key', emailPlugin.erase); } // Address routes diff --git a/plugins/emailstore.js b/plugins/emailstore.js index 5ca06410..b3d5db25 100644 --- a/plugins/emailstore.js +++ b/plugins/emailstore.js @@ -315,6 +315,48 @@ }); }; + emailPlugin.deleteByEmailAndKey = function deleteByEmailAndKey(email, key, callback) { + emailPlugin.db.del(valueKey(email, key), function(error) { + if (error) { + if (error.notFound) { + return callback(emailPlugin.errors.NOT_FOUND); + } else { + logger.error(error); + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + } + return callback(); + }); + }; + + emailPlugin.deleteWholeProfile = function deleteWholeProfile(email, callback) { + var dismissNotFound = function(callback) { + return function(error, result) { + if (error && error.notFound) { + return callback(); + } + return callback(error, result); + }; + }; + async.parallel([ + function(callback) { + emailPlugin.db.del(emailToPassphrase(email), dismissNotFound(callback)); + }, + function(callback) { + emailPlugin.db.del(pendingKey(email), dismissNotFound(callback)); + }, + function(callback) { + emailPlugin.db.del(validatedKey(email), dismissNotFound(callback)); + } + ], function(err) { + if (err) { + logger.error(err); + return callback(emailPlugin.errors.INTERNAL_ERROR); + } + return callback(); + }); + }; + /** * Store a record in the database. The underlying database is merely a levelup instance (a key * value store) that uses the email concatenated with the secret as a key to store the record. @@ -424,7 +466,6 @@ }); }; - emailPlugin.getCredentialsFromRequest = function(request) { var auth = request.header('authorization'); if (!auth) { @@ -444,51 +485,103 @@ }; }; - - emailPlugin.addValidationHeader = function(response, email, callback) { - emailPlugin.db.get(validatedKey(email), function(err, value) { - if (err && !err.notFound) + if (err && !err.notFound) { return callback(err); + } - if (value) + if (value) { return callback(); + } response.set('X-Email-Needs-Validation', 'true'); - return callback(null, value || false); + return callback(null, value); }); }; + function authorizedRequest(withKey, callback) { + return function(request, response) { + var credentialsResult = emailPlugin.getCredentialsFromRequest(request); + if (_.contains(emailPlugin.errors, credentialsResult)) { + return emailPlugin.returnError(credentialsResult, response); + } + var email = credentialsResult.email; + var passphrase = credentialsResult.passphrase; + var key; + if (withKey) { + key = request.param('key'); + } + + if (!passphrase || !email || (withKey && !key)) { + return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); + } + return callback(email, passphrase, key, request, response); + }; + } + /** * Retrieve a record from the database */ - emailPlugin.retrieve = function(request, response) { - var credentialsResult = emailPlugin.getCredentialsFromRequest(request); - if (_.contains(emailPlugin.errors, credentialsResult)) { - return emailPlugin.returnError(credentialsResult); - - } - var email = credentialsResult.email; - var passphrase = credentialsResult.passphrase; - - var key = request.param('key'); - if (!passphrase || !email || !key) { - return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); - } - - emailPlugin.retrieveDataByEmailAndPassphrase(email, key, passphrase, function(err, value) { - if (err) - return emailPlugin.returnError(err, response); - - emailPlugin.addValidationHeader(response, email, function(err) { - if (err) + emailPlugin.retrieve = authorizedRequest(true, + function(email, passphrase, key, request, response) { + emailPlugin.retrieveDataByEmailAndPassphrase(email, key, passphrase, function(err, value) { + if (err) { return emailPlugin.returnError(err, response); - - response.send(value).end(); + } + emailPlugin.addValidationHeader(response, email, function(err) { + if (err) { + return emailPlugin.returnError(err, response); + } + response.send(value).end(); + }); }); - }); - }; + } + ); + + /** + * Remove a record from the database + */ + emailPlugin.erase = authorizedRequest(true, + function(email, passphrase, key, request, response) { + emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + if (err || !matches) { + return emailPlugin.returnError(emailPlugin.errors.INVALID_CODE, response); + } + emailPlugin.deleteByEmailAndKey(email, key, function(err, value) { + if (err) { + return emailPlugin.returnError(err, response); + } else { + return response.json({success: true}).end(); + }; + }); + }); + } + ); + + /** + * Remove a whole profile from the database + * + * @TODO: This looks very similar to the method above + */ + emailPlugin.eraseProfile = authorizedRequest(false, + function(email, passphrase, unused_key, request, response) { + + emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + if (err || !matches) { + return emailPlugin.returnError(emailPlugin.errors.INVALID_CODE, response); + } + emailPlugin.deleteWholeProfile(email, function(err, value) { + if (err) { + return emailPlugin.returnError(err, response); + } else { + return response.json({success: true}).end(); + }; + }); + }); + } + ); + /** * Marks an email as validated diff --git a/test/test.EmailStore.js b/test/test.EmailStore.js index 4cedf13c..4281ba73 100644 --- a/test/test.EmailStore.js +++ b/test/test.EmailStore.js @@ -377,6 +377,59 @@ describe('emailstore test', function() { }); }); + describe('removing items', function() { + var fakeEmail = 'fake@email.com'; + var fakeKey = 'nameForData'; + beforeEach(function() { + leveldb_stub.del = sinon.stub(); + }); + it('deletes a stored element (key)', function(done) { + leveldb_stub.del.onFirstCall().callsArg(1); + plugin.deleteByEmailAndKey(fakeEmail, fakeKey, function(err) { + expect(err).to.be.undefined; + done(); + }); + }); + it('returns NOT FOUND if trying to delete a stored element by key', function(done) { + leveldb_stub.del.onFirstCall().callsArgWith(1, {notFound: true}); + plugin.deleteByEmailAndKey(fakeEmail, fakeKey, function(err) { + err.should.equal(plugin.errors.NOT_FOUND); + done(); + }); + }); + it('returns INTERNAL_ERROR if an unexpected error ocurrs', function(done) { + leveldb_stub.del.onFirstCall().callsArgWith(1, {unexpected: true}); + plugin.deleteByEmailAndKey(fakeEmail, fakeKey, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + it('can delete a whole profile (validation data and passphrase)', function(done) { + leveldb_stub.del.callsArg(1); + plugin.deleteWholeProfile(fakeEmail, function(err) { + expect(err).to.be.undefined; + leveldb_stub.del.callCount.should.equal(3); + done(); + }); + }); + it('dismisses not found errors', function(done) { + leveldb_stub.del.callsArg(1); + leveldb_stub.del.onSecondCall().callsArgWith(1, {notFound: true}); + plugin.deleteWholeProfile(fakeEmail, function(err) { + expect(err).to.be.undefined; + done(); + }); + }); + it('returns internal error if something goes awry', function(done) { + leveldb_stub.del.callsArg(1); + leveldb_stub.del.onSecondCall().callsArgWith(1, {unexpected: true}); + plugin.deleteWholeProfile(fakeEmail, function(err) { + err.should.equal(plugin.errors.INTERNAL_ERROR); + done(); + }); + }); + }); + describe('when retrieving data', function() { it('should validate the secret and return the data', function() { From 446ec49590eb5aa0cfa7936adbd2b2bbe2936e31 Mon Sep 17 00:00:00 2001 From: Matias Alejo Garcia Date: Thu, 13 Nov 2014 11:39:17 -0300 Subject: [PATCH 2/2] refactor: authorize request --- plugins/emailstore.js | 204 ++++++++++++++++++++++------------------ test/test.EmailStore.js | 109 +++++++++++++++++---- 2 files changed, 205 insertions(+), 108 deletions(-) diff --git a/plugins/emailstore.js b/plugins/emailstore.js index b3d5db25..154ee3a9 100644 --- a/plugins/emailstore.js +++ b/plugins/emailstore.js @@ -302,19 +302,6 @@ }); }; - emailPlugin.retrieveDataByEmailAndPassphrase = function(email, key, passphrase, callback) { - emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { - if (err) { - return callback(err); - } - if (matches) { - return emailPlugin.retrieveByEmailAndKey(email, key, callback); - } else { - return callback(emailPlugin.errors.INVALID_CODE); - } - }); - }; - emailPlugin.deleteByEmailAndKey = function deleteByEmailAndKey(email, key, callback) { emailPlugin.db.del(valueKey(email, key), function(error) { if (error) { @@ -339,6 +326,7 @@ }; }; async.parallel([ + function(callback) { emailPlugin.db.del(emailToPassphrase(email), dismissNotFound(callback)); }, @@ -500,87 +488,111 @@ }); }; - function authorizedRequest(withKey, callback) { - return function(request, response) { - var credentialsResult = emailPlugin.getCredentialsFromRequest(request); - if (_.contains(emailPlugin.errors, credentialsResult)) { - return emailPlugin.returnError(credentialsResult, response); - } - var email = credentialsResult.email; - var passphrase = credentialsResult.passphrase; - var key; - if (withKey) { - key = request.param('key'); + emailPlugin.authorizeRequest = function(request, withKey, callback) { + var credentialsResult = emailPlugin.getCredentialsFromRequest(request); + if (_.contains(emailPlugin.errors, credentialsResult)) { + return callback(credentialsResult); + } + + var email = credentialsResult.email; + var passphrase = credentialsResult.passphrase; + var key; + if (withKey) { + key = request.param('key'); + } + + if (!passphrase || !email || (withKey && !key)) { + return callback(emailPlugin.errors.MISSING_PARAMETER); + } + + emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + if (err) { + return callback(err); } - if (!passphrase || !email || (withKey && !key)) { - return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); + if (!matches) { + return callback(emailPlugin.errors.INVALID_CODE); } - return callback(email, passphrase, key, request, response); - }; - } + + return callback(null, email, key); + }); + }; + + emailPlugin.authorizeRequestWithoutKey = function(request, callback) { + emailPlugin.authorizeRequest(request, false, callback); + }; + + emailPlugin.authorizeRequestWithKey = function(request, callback) { + emailPlugin.authorizeRequest(request, true, callback); + }; /** * Retrieve a record from the database */ - emailPlugin.retrieve = authorizedRequest(true, - function(email, passphrase, key, request, response) { - emailPlugin.retrieveDataByEmailAndPassphrase(email, key, passphrase, function(err, value) { + emailPlugin.retrieve = function(request, response) { + emailPlugin.authorizeRequestWithKey(request, function(err, email, key) { + if (err) { + return emailPlugin.returnError(err, response); + } + + emailPlugin.retrieveByEmailAndKey(email, key, function(err, value) { if (err) { return emailPlugin.returnError(err, response); } + emailPlugin.addValidationHeader(response, email, function(err) { if (err) { return emailPlugin.returnError(err, response); } + response.send(value).end(); }); }); - } - ); + }); + }; /** * Remove a record from the database */ - emailPlugin.erase = authorizedRequest(true, - function(email, passphrase, key, request, response) { - emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { - if (err || !matches) { - return emailPlugin.returnError(emailPlugin.errors.INVALID_CODE, response); - } - emailPlugin.deleteByEmailAndKey(email, key, function(err, value) { - if (err) { - return emailPlugin.returnError(err, response); - } else { - return response.json({success: true}).end(); - }; - }); + emailPlugin.erase = function(request, response) { + emailPlugin.authorizeRequestWithKey(request, function(err, email, key) { + if (err) { + return emailPlugin.returnError(err, response); + } + emailPlugin.deleteByEmailAndKey(email, key, function(err, value) { + if (err) { + return emailPlugin.returnError(err, response); + } else { + return response.json({ + success: true + }).end(); + }; }); - } - ); + }); + }; /** * Remove a whole profile from the database * * @TODO: This looks very similar to the method above */ - emailPlugin.eraseProfile = authorizedRequest(false, - function(email, passphrase, unused_key, request, response) { - - emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { - if (err || !matches) { - return emailPlugin.returnError(emailPlugin.errors.INVALID_CODE, response); - } - emailPlugin.deleteWholeProfile(email, function(err, value) { - if (err) { - return emailPlugin.returnError(err, response); - } else { - return response.json({success: true}).end(); - }; - }); + emailPlugin.eraseProfile = function(request, response) { + emailPlugin.authorizeRequestWithoutKey(request, function(err, email) { + if (err) { + return emailPlugin.returnError(err, response); + } + + emailPlugin.deleteWholeProfile(email, function(err, value) { + if (err) { + return emailPlugin.returnError(err, response); + } else { + return response.json({ + success: true + }).end(); + }; }); - } - ); + }); + }; /** @@ -642,32 +654,28 @@ * @param {Express.Response} response */ emailPlugin.changePassphrase = function(request, response) { - var credentialsResult = emailPlugin.getCredentialsFromRequest(request); - if (_.contains(emailPlugin.errors, credentialsResult)) { - return emailPlugin.returnError(credentialsResult); - } - var email = credentialsResult.email; - var passphrase = credentialsResult.passphrase; - var queryData = ''; - request.on('data', function(data) { - queryData += data; - if (queryData.length > MAX_ALLOWED_STORAGE) { - queryData = ''; - response.writeHead(413, { - 'Content-Type': 'text/plain' - }).end(); - request.connection.destroy(); + emailPlugin.authorizeRequestWithoutKey(request, function(err, email) { + + if (err) { + return emailPlugin.returnError(err, response); } - }).on('end', function() { - var params = querystring.parse(queryData); - var newPassphrase = params.newPassphrase; - if (!email || !passphrase || !newPassphrase) { - return emailPlugin.returnError(emailPlugin.errors.INVALID_REQUEST, response); - } - emailPlugin.checkPassphrase(email, passphrase, function(error) { - if (error) { - return emailPlugin.returnError(error, response); + + var queryData = ''; + request.on('data', function(data) { + queryData += data; + if (queryData.length > MAX_ALLOWED_STORAGE) { + queryData = ''; + response.writeHead(413, { + 'Content-Type': 'text/plain' + }).end(); + request.connection.destroy(); + } + }).on('end', function() { + var params = querystring.parse(queryData); + var newPassphrase = params.newPassphrase; + if (!newPassphrase) { + return emailPlugin.returnError(emailPlugin.errors.INVALID_REQUEST, response); } emailPlugin.savePassphrase(email, newPassphrase, function(error) { if (error) { @@ -682,7 +690,23 @@ }; + // // Backwards compatibility + // + + emailPlugin.oldRetrieveDataByEmailAndPassphrase = function(email, key, passphrase, callback) { + emailPlugin.checkPassphrase(email, passphrase, function(err, matches) { + if (err) { + return callback(err); + } + if (matches) { + return emailPlugin.retrieveByEmailAndKey(email, key, callback); + } else { + return callback(emailPlugin.errors.INVALID_CODE); + } + }); + }; + emailPlugin.oldRetrieve = function(request, response) { var email = request.param('email'); @@ -692,7 +716,7 @@ return emailPlugin.returnError(emailPlugin.errors.MISSING_PARAMETER, response); } - emailPlugin.retrieveDataByEmailAndPassphrase(email, key, secret, function(err, value) { + emailPlugin.oldRetrieveDataByEmailAndPassphrase(email, key, secret, function(err, value) { if (err) { return emailPlugin.returnError(err, response); } diff --git a/test/test.EmailStore.js b/test/test.EmailStore.js index 4281ba73..ffe2229d 100644 --- a/test/test.EmailStore.js +++ b/test/test.EmailStore.js @@ -433,39 +433,115 @@ describe('emailstore test', function() { describe('when retrieving data', function() { it('should validate the secret and return the data', function() { - request.header = sinon.stub(); - request.header.onFirstCall().returns(new Buffer('email:pass', 'utf8').toString('base64')); request.param.onFirstCall().returns('key'); - plugin.retrieveDataByEmailAndPassphrase = sinon.stub(); - plugin.retrieveDataByEmailAndPassphrase.onFirstCall().callsArgWith(3, null, 'encrypted'); + plugin.authorizeRequestWithKey = sinon.stub().callsArgWith(1,null, 'email','key'); + plugin.retrieveByEmailAndKey = sinon.stub().yields(null, 'encrypted'); + response.send.onFirstCall().returnsThis(); plugin.addValidationHeader = sinon.stub().callsArg(2); plugin.retrieve(request, response); - request.header.calledOnce.should.equal(true); response.send.calledOnce.should.equal(true); - assert(request.header.firstCall.args[0] === 'authorization'); - assert(plugin.retrieveDataByEmailAndPassphrase.firstCall.args[0] === 'email'); - assert(plugin.retrieveDataByEmailAndPassphrase.firstCall.args[1] === 'key'); - assert(plugin.retrieveDataByEmailAndPassphrase.firstCall.args[2] === 'pass'); + assert(plugin.retrieveByEmailAndKey.firstCall.args[0] === 'email'); + assert(plugin.retrieveByEmailAndKey.firstCall.args[1] === 'key'); assert(response.send.firstCall.args[0] === 'encrypted'); assert(response.end.calledOnce); }); }); - describe('changing the user password', function() { - - var originalCredentials = plugin.getCredentialsFromRequest; + describe('authorizing requests', function() { + var originalCredentials; beforeEach(function() { + originalCredentials = plugin.getCredentialsFromRequest; + plugin.getCredentialsFromRequest = sinon.mock(); plugin.getCredentialsFromRequest.onFirstCall().returns({ email: 'email', - passphrase: 'passphrase' + passphrase: 'pass' }); + request.param.onFirstCall().returns('key'); + + request.on = sinon.stub(); + request.on.onFirstCall().callsArgWith(1, 'newPassphrase=newPassphrase'); + request.on.onFirstCall().returns(request); + request.on.onSecondCall().callsArg(1); + plugin.checkPassphrase = sinon.stub().callsArgWith(2,null, true); + + }); + + it('should authorize a request', function(done){ + plugin.authorizeRequest(request, false, function(err, email, key) { + expect(err).to.be.null; + expect(key).to.be.undefined; + email.should.be.equal('email'); + done(); + }); + }); + it('should authorize a request with key', function(done){ + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: 'email', + passphrase: 'pass', + }); + plugin.authorizeRequest(request, true, function(err, email, key) { + expect(err).to.be.null; + email.should.be.equal('email'); + key.should.be.equal('key'); + done(); + }); + }); + + it('should not authorize a request when param are missing', function(done){ + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: 'email', + }); + + plugin.authorizeRequest(request, false, function(err, email, key) { + expect(err).not.to.be.null; + expect(key).to.be.undefined; + expect(email).to.be.undefined; + done(); + }); + }); + it('should not authorize a request when param are missing (case2)', function(done){ + plugin.getCredentialsFromRequest.onFirstCall().returns({ + passphrase: 'pass' + }); + + plugin.authorizeRequest(request, false, function(err, email, key) { + expect(err).not.to.be.null; + expect(key).to.be.undefined; + expect(email).to.be.undefined; + done(); + }); + }); + it('should not authorize a request when param are missing (case3)', function(done){ + request.param.onFirstCall().returns(undefined); + plugin.getCredentialsFromRequest.onFirstCall().returns({ + email: 'email', + passphrase: 'pass' + }); + plugin.authorizeRequest(request, true, function(err, email, key) { + expect(err).not.to.be.null; + expect(key).to.be.undefined; + expect(email).to.be.undefined; + done(); + }); + }); + + + after(function() { + plugin.getCredentialsFromRequest = originalCredentials; + }); + }); + + describe('changing the user password', function() { + + + beforeEach(function() { request.on = sinon.stub(); request.on.onFirstCall().callsArgWith(1, 'newPassphrase=newPassphrase'); request.on.onFirstCall().returns(request); @@ -478,7 +554,7 @@ describe('emailstore test', function() { it('should validate the previous passphrase', function() { response.status.onFirstCall().returnsThis(); response.json.onFirstCall().returnsThis(); - plugin.checkPassphrase.onFirstCall().callsArgWith(2, 'error'); + plugin.authorizeRequestWithoutKey = sinon.stub().callsArgWith(1,'error'); plugin.changePassphrase(request, response); @@ -489,6 +565,7 @@ describe('emailstore test', function() { it('should change the passphrase', function() { response.json.onFirstCall().returnsThis(); + plugin.authorizeRequestWithoutKey = sinon.stub().callsArgWith(1,null, 'email'); plugin.checkPassphrase.onFirstCall().callsArgWith(2, null); plugin.savePassphrase.onFirstCall().callsArgWith(2, null); @@ -496,9 +573,5 @@ describe('emailstore test', function() { assert(response.json.calledOnce); assert(response.end.calledOnce); }); - - after(function() { - plugin.getCredentialsFromRequest = originalCredentials; - }); }); });