Skip to content

Commit

Permalink
feat: diff user docs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Johannes J. Schmidt committed Dec 28, 2015
1 parent dd5748d commit 11be415
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 20 deletions.
55 changes: 46 additions & 9 deletions index.js
Expand Up @@ -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')
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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"
},
Expand Down
7 changes: 7 additions & 0 deletions 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": []
}
7 changes: 7 additions & 0 deletions test/fixtures/user.json
@@ -0,0 +1,7 @@
{
"_id": "org.couchdb.user:couchdb-push-testuser",
"type": "user",
"name": "couchdb-push-testuser",
"password": "secret",
"roles": []
}
65 changes: 54 additions & 11 deletions test/push-test.js
Expand Up @@ -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')
Expand All @@ -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')

Expand All @@ -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')

Expand All @@ -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')
Expand All @@ -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')

Expand All @@ -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')
Expand Down

0 comments on commit 11be415

Please sign in to comment.