From 11be4158b4e2fb9ee72a992fd2bc4094619838f0 Mon Sep 17 00:00:00 2001 From: "Johannes J. Schmidt" Date: Mon, 28 Dec 2015 14:08:52 +0100 Subject: [PATCH] feat: diff user docs user docs have special fields, `derived_key`, `iterations`, `password_scheme` and `salt` which are calculated server side based on the `password` property. This change ignores those fields for comparison as well as the password field as long as the password has not been changed. This prevents couchdb-push to update user docs even if they have not changed. The resulting doc will have a different `salt` and `derived_key` and existing sessions will be invalid. Closes #25 --- index.js | 55 +++++++++++++++++++++++----- package.json | 1 + test/fixtures/changed-user.json | 7 ++++ test/fixtures/user.json | 7 ++++ test/push-test.js | 65 +++++++++++++++++++++++++++------ 5 files changed, 115 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/changed-user.json create mode 100644 test/fixtures/user.json diff --git a/index.js b/index.js index 4151b09..ac70aac 100644 --- a/index.js +++ b/index.js @@ -2,8 +2,9 @@ // (c) 2014 Johannes J. Schmidt var crypto = require('crypto') -var assert = require('assert') var async = require('async') +var omit = require('lodash/object/omit') +var isEqual = require('lodash/lang/isEqual') var nanoOption = require('nano-option') var compile = require('couchdb-compile') var ensure = require('couchdb-ensure') @@ -75,18 +76,54 @@ module.exports = function push (db, source, options, callback) { } } - try { - assert.deepEqual(doc, existingDoc) - if (options.multipart) { - assert.equal(attachments.length, 0) - } + // cannot diff multipart attachments + if (options.multipart && attachments.length > 0) { + return pushDoc(doc, attachments, done) + } + + hasChanged(doc, existingDoc, function (error, changed) { + if (error) return done(error) + + if (changed) return pushDoc(doc, attachments, done) - done(null, { ok: true, id: doc._id, rev: doc._rev, unchanged: true }) - } catch (e) { - pushDoc(doc, attachments, done) + done(null, { + ok: true, + id: doc._id, + rev: doc._rev, + unchanged: true + }) + }) + } + + function hasChanged (doc, existingDoc, callback) { + if (isUserDoc(doc) && doc.name && doc.password) { + confirmSession(doc.name, doc.password, function (error, result) { + if (error) { + if (error.statusCode === 401) return callback(null, true) + return callback(error) + } + + var userDocToCompare = omit(doc, 'password') + var existingDocToCompare = omit(existingDoc, 'derived_key', 'iterations', 'password_scheme', 'salt') + + callback(null, !isEqual(userDocToCompare, existingDocToCompare)) + }) + } else { + callback(null, !isEqual(doc, existingDoc)) } } + // TChecking against `_users` is not acurate, because the users db can be configured: + // [couch_httpd_auth] + // authentication_db = _users + function isUserDoc (doc) { + return db.config.db === '_users' + } + + function confirmSession (name, password, done) { + db.auth(name, password, done) + } + function getDoc (doc, attachments, done) { db.get(doc._id, function (err, response) { if (err && err.statusCode === 404) { diff --git a/package.json b/package.json index 446913c..4ac85e7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "chokidar": "^1.2.0", "couchdb-compile": "^1.6.2", "couchdb-ensure": "^1.3.1", + "lodash": "^3.10.1", "minimist": "^1.2.0", "nano-option": "^1.1.1" }, diff --git a/test/fixtures/changed-user.json b/test/fixtures/changed-user.json new file mode 100644 index 0000000..77aa7c1 --- /dev/null +++ b/test/fixtures/changed-user.json @@ -0,0 +1,7 @@ +{ + "_id": "org.couchdb.user:couchdb-push-testuser", + "type": "user", + "name": "couchdb-push-testuser", + "password": "secret-changed", + "roles": [] +} diff --git a/test/fixtures/user.json b/test/fixtures/user.json new file mode 100644 index 0000000..f63ed5d --- /dev/null +++ b/test/fixtures/user.json @@ -0,0 +1,7 @@ +{ + "_id": "org.couchdb.user:couchdb-push-testuser", + "type": "user", + "name": "couchdb-push-testuser", + "password": "secret", + "roles": [] +} diff --git a/test/push-test.js b/test/push-test.js index cb686fd..0978a41 100644 --- a/test/push-test.js +++ b/test/push-test.js @@ -7,17 +7,27 @@ var path = require('path') var test = require('tap').test var push = require('..') -var sources = [ +var docs = [ path.join(__dirname, 'fixtures/doc.json'), path.join(__dirname, 'fixtures/otherdoc.json') ] -var source = sources[0] +var userdocs = [ + path.join(__dirname, 'fixtures/user.json'), + path.join(__dirname, 'fixtures/changed-user.json') +] var couch = nano(url) var db = couch.use(dbname) +function rm (db, id, callback) { + db.get(id, function (error, doc) { + if (error) return callback(null) + db.destroy(id, doc._id, callback) + }) +} + test('database not present', function (t) { couch.db.destroy(dbname, function () { - push(url + '/' + dbname, source, function (error, response) { + push(url + '/' + dbname, docs[0], function (error, response) { t.error(error, 'no error') t.equal(response.ok, true, 'response is ok') t.type(response.rev, 'string', 'response has rev') @@ -31,7 +41,7 @@ test('database not present', function (t) { test('database is present', function (t) { couch.db.create(dbname, function () { - push(url + '/' + dbname, source, function (error, response) { + push(url + '/' + dbname, docs[0], function (error, response) { t.error(error, 'no error') t.equal(response.ok, true, 'response is ok') @@ -41,7 +51,7 @@ test('database is present', function (t) { }) test('url as nano object', function (t) { - push(db, source, function (error, response) { + push(db, docs[0], function (error, response) { t.error(error, 'no error') t.equal(response.ok, true, 'response is ok') @@ -51,9 +61,27 @@ test('url as nano object', function (t) { test('doc unchanged', function (t) { couch.db.destroy(dbname, function () { - push(url + '/' + dbname, source, function (error, response) { + push(url + '/' + dbname, docs[0], function (error, response) { + t.error(error, 'no error') + push(url + '/' + dbname, docs[0], function (error, response) { + t.error(error, 'no error') + t.equal(response.ok, true, 'response is ok') + t.type(response.rev, 'string', 'response has rev') + t.type(response.id, 'string', 'response has id') + t.equal(response.unchanged, true, 'response is unchanged') + + t.end() + }) + }) + }) +}) + +test('user unchanged', function (t) { + rm(couch.use('_users'), userdocs[0]._id, function (error) { + t.error(error, 'no error') + push(url + '/_users', userdocs[0], function (error, response) { t.error(error, 'no error') - push(url + '/' + dbname, source, function (error, response) { + push(url + '/_users', userdocs[0], function (error, response) { t.error(error, 'no error') t.equal(response.ok, true, 'response is ok') t.type(response.rev, 'string', 'response has rev') @@ -66,10 +94,25 @@ test('doc unchanged', function (t) { }) }) +test('user password changed', function (t) { + push(url + '/_users', userdocs[0], function (error, response) { + t.error(error, 'no error') + var rev = response.rev + push(url + '/_users', userdocs[1], function (error, response) { + t.error(error, 'no error') + t.equal(response.ok, true, 'response is ok') + t.notOk(response.unchanged, 'response is unchanged') + t.ok(rev !== response.rev, 'rev has been changed') + + t.end() + }) + }) +}) + test('database containing a slash', function (t) { var name = dbname + '/one' couch.db.destroy(name, function () { - push(url + '/' + encodeURIComponent(name), source, function (error, response) { + push(url + '/' + encodeURIComponent(name), docs[0], function (error, response) { t.error(error, 'no error') t.equal(response.ok, true, 'response is ok') @@ -80,11 +123,11 @@ test('database containing a slash', function (t) { test('concurrency', function (t) { couch.db.destroy(dbname, function () { - async.map(sources, function (source, done) { - push(url + '/' + dbname, source, done) + async.map(docs, function (doc, done) { + push(url + '/' + dbname, doc, done) }, function (error, responses) { t.error(error, 'no error') - t.equal(responses.length, sources.length, 'correct # of docs pushed') + t.equal(responses.length, docs.length, 'correct # of docs pushed') responses.forEach(function (response) { t.equal(typeof response, 'object', 'response is object') t.equal(response.ok, true, 'response is ok')