Skip to content

Commit

Permalink
feat: add token methods
Browse files Browse the repository at this point in the history
  • Loading branch information
targos committed Jul 25, 2016
1 parent 881648c commit b0678f2
Show file tree
Hide file tree
Showing 10 changed files with 160 additions and 7 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"eslint": "eslint src bin test",
"eslint-fix": "npm run eslint -- --fix",
"start": "./bin/rest-on-couch.js server",
"test": "npm run test-mocha && npm run eslint && sh test/checkOnly.sh",
"test": "npm run compile && npm run test-mocha && npm run eslint && sh test/checkOnly.sh",
"test-mocha": "mocha --timeout 5000 --require should --require ./test/setup --reporter mocha-better-spec-reporter --recursive",
"test-cov": "istanbul cover -x '**/design/**' _mocha -- --require should --require ./test/setup"
},
Expand Down Expand Up @@ -64,6 +64,7 @@
"passport-google-oauth20": "^1.0.0",
"passport-ldapauth": "^0.5.0",
"passport-local": "^1.0.0",
"randomatic": "^1.1.5",
"raw-body": "^2.1.5",
"request-promise": "^4.0.2",
"superagent": "^2.0.0",
Expand Down
2 changes: 1 addition & 1 deletion src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module.exports = {
DESIGN_DOC_NAME: 'app',
DESIGN_DOC_ID: '_design/app',
DESIGN_DOC_VERSION: 11,
DESIGN_DOC_VERSION: 12,
RIGHTS_DOC_ID: 'rights',
DEFAULT_GROUPS_DOC_ID: 'defaultGroups'
};
14 changes: 12 additions & 2 deletions src/design/validateDocUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = function (newDoc, oldDoc, userCtx) {
if (newDoc._deleted) {
return;
}
var validTypes = ['entry', 'group', 'db', 'log', 'user'];
var validTypes = ['entry', 'group', 'db', 'log', 'user', 'token'];
var validRights = ['create', 'read', 'write', 'createGroup'];
// see http://emailregex.com/
var validEmail = /^[-a-z0-9~!$%^&*_=+}{\'?]+(\.[-a-z0-9~!$%^&*_=+}{\'?]+)*@([a-z0-9_][-a-z0-9_]*(\.[-a-z0-9_]+)*\.(aero|arpa|biz|com|coop|edu|gov|info|int|mil|museum|name|net|org|pro|travel|mobi|[a-z][a-z])|([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}))(:[0-9]{1,5})?$/i;
Expand All @@ -23,7 +23,7 @@ module.exports = function (newDoc, oldDoc, userCtx) {
}

if (!newDoc.$type || validTypes.indexOf(newDoc.$type) === -1) {
throw ({forbidden: 'Invalid type'});
throw ({forbidden: 'Invalid type: ' + newDoc.$type});
}
if (oldDoc && newDoc.$type !== oldDoc.$type) {
throw ({forbidden: 'Cannot change the type of document'});
Expand Down Expand Up @@ -87,5 +87,15 @@ module.exports = function (newDoc, oldDoc, userCtx) {
if (!newDoc.user || !validEmail.test(newDoc.user)) {
throw ({forbidden: 'user must have user property, which must be an email'});
}
} else if (newDoc.$type === 'token') {
if (oldDoc) {
throw ({forbidden: 'Tokens are immutable'});
}
if (newDoc.$kind !== 'entry') {
throw ({forbidden: 'Only entry tokens are supported'});
}
if (!newDoc.$id || !newDoc.$owner || !newDoc.uuid || (typeof newDoc.$creationDate !== 'number') || !Array.isArray(newDoc.rights)) {
throw ({forbidden: 'token is missing fields'});
}
}
};
14 changes: 14 additions & 0 deletions src/design/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,17 @@ views.user = {
}
}
};

views.tokenById = {
map: function (doc) {
if (doc.$type !== 'token') return;
emit(doc.$id);
}
};

views.tokenByOwner = {
map: function (doc) {
if (doc.$type !== 'token') return;
emit(doc.$owner);
}
};
40 changes: 39 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const CouchError = require('./util/CouchError');
const constants = require('./constants');
const getDesignDoc = require('./design/app');
const nanoPromise = require('./util/nanoPromise');
const token = require('./util/token');
const log = require('./couch/log');
const getConfig = require('./config/config').getConfig;
const globalRightTypes = ['read', 'write', 'create', 'createGroup'];
Expand Down Expand Up @@ -748,7 +749,7 @@ class Couch {
}

deleteEntryById(id, user) {
debug(`deleteEntryById (${id}, ${user}`);
debug(`deleteEntryById (${id}, ${user})`);
return this.getEntryByIdAndRights(id, user, 'delete')
.then(doc => nanoPromise.destroyDocument(this._db, doc._id));
}
Expand All @@ -762,6 +763,43 @@ class Couch {
debug(`getLogs (${epoch}`);
return this.open().then(() => log.getLogs(this._db, epoch));
}

async createEntryToken(user, uuid) {
debug(`createEntryToken (${user}, ${uuid})`);
await this.open();
// We need write right to create a token. This will throw if not.
await this.getEntryByUuidAndRights(uuid, user, 'write');
return await token.createEntryToken(this._db, user, uuid, 'read');
}

async deleteToken(user, tokenId) {
debug(`deleteToken (${user}, ${tokenId})`);
await this.open();
const tokenValue = await token.getToken(this._db, tokenId);
if (!tokenValue) {
throw new CouchError('token not found', 'not found');
}
if (tokenValue.$owner !== user) {
throw new CouchError('only owner can delete a token', 'unauthorized');
}
await token.destroyToken(this._db, tokenValue._id, tokenValue._rev);
}

async getToken(tokenId) {
debug(`getToken (${tokenId})`);
await this.open();
const tokenValue = await token.getToken(this._db, tokenId);
if (!tokenValue) {
throw new CouchError('token not found', 'not found');
}
return tokenValue;
}

async getTokens(user) {
debug(`getTokens (${user})`);
await this.open();
return await token.getTokens(this._db, user);
}
}

const databaseCache = new Map();
Expand Down
45 changes: 45 additions & 0 deletions src/util/token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';

const randomatic = require('randomatic');
const getRandomToken = () => randomatic('Aa0', 32);

const CouchError = require('./CouchError');
const nanoPromise = require('./nanoPromise');

exports.createEntryToken = async function createEntryToken(db, user, uuid, rights) {
if (typeof rights === 'string') {
rights = [rights];
} else if (!Array.isArray(rights)) {
throw new CouchError('rights must be an array');
}
const token = {
$type: 'token',
$kind: 'entry',
$id: getRandomToken(),
$owner: user,
$creationDate: Date.now(),
uuid,
rights
};
await nanoPromise.insertDocument(db, token);
return token;
};

exports.getToken = async function getToken(db, tokenId) {
const result = await nanoPromise.queryView(db, 'tokenById', {key: tokenId, include_docs: true}, {onlyDoc: true});
if (result.length === 0) {
return null;
} else if (result.length === 1) {
return result[0];
} else {
throw new CouchError('multiple tokens with the same ID', 'fatal');
}
};

exports.getTokens = async function getTokens(db, user) {
return await nanoPromise.queryView(db, 'tokenByOwner', {key: user, include_docs: true}, {onlyDoc: true});
};

exports.destroyToken = async function destroyToken(db, tokenId, rev) {
return await nanoPromise.destroyDocument(db, tokenId, rev);
};
1 change: 1 addition & 0 deletions test/data/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function populate(db) {
$type: 'entry',
$owners: ['b@b.com', 'groupA', 'groupB'],
$id: 'A',
_id: 'A',
$creationDate: 0,
$modificationDate: 0,
$content: {}
Expand Down
2 changes: 1 addition & 1 deletion test/design/validateDocUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ global.isArray = function (obj) {
describe('validate_doc_update', function () {
describe('general', function () {
it('$type', function () {
assert({$type: 'abc'}, null, 'Invalid type');
assert({$type: 'abc'}, null, 'Invalid type: abc');
assert({$type: 'entry'}, {$type: 'group'}, /Cannot change the type/);
});
});
Expand Down
2 changes: 1 addition & 1 deletion test/rest-api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ describe('basic rest-api as b@b.com', function () {

it('non-existent document cannot be updated', function () {
// document with uuid A does not exist
return request.put('/db/test/entry/A').send({$id: 'A', $content: {}})
return request.put('/db/test/entry/NOTEXIST').send({$id: 'NOTEXIST', $content: {}})
.expect(404);
});

Expand Down
44 changes: 44 additions & 0 deletions test/token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

const data = require('./data/data');

describe.only('token methods', function () {
before(data);
it('user should be able to create and get tokens', function () {
return Promise.all([couch.createEntryToken('a@a.com', 'A'), couch.createEntryToken('a@a.com', 'B')])
.then(tokens => {
tokens[0].$id.should.not.equal(tokens[1].$id);
const token = tokens[0];
token.$type.should.equal('token');
token.$kind.should.equal('entry');
token.$id.length.should.equal(32);
token.$owner.should.equal('a@a.com');
token.uuid.should.equal('A');
token.rights.should.eql(['read']);
return couch.getToken(token.$id).then(gotToken => {
gotToken.$id.should.equal(token.$id);
return couch.getTokens().then(tokens => {
tokens.length.should.equal(2);
});
});
});
});

it('user should be able to create and destroy tokens', function () {
return couch.createEntryToken('a@a.com', 'A')
.then(token => {
return couch.getToken(token.$id).then(gotToken => {
gotToken.$id.should.equal(token.$id);
return couch.deleteToken('a@a.com', token.$id).then(() => {
return couch.getToken(token.$id).should.be.rejected();
});
});
});
});

it('user should not be able to create a token without write right', function () {
return couch.createEntryToken('a@a.com', 'C').should.be.rejected();
});


});

0 comments on commit b0678f2

Please sign in to comment.