Skip to content

Commit

Permalink
feat: add audit actions feature and audit login attempts
Browse files Browse the repository at this point in the history
Refs: #171
  • Loading branch information
targos committed Nov 1, 2018
1 parent 300704f commit 6d1406c
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 21 deletions.
1 change: 0 additions & 1 deletion bin/rest-on-couch-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const program = require('commander');

const debug = require('../src/util/debug')('bin:server');
const server = require('../src/server/server');
require('../src/util/load')();

program
.option('-c --config <path>', 'Path to custom config file')
Expand Down
55 changes: 55 additions & 0 deletions src/audit/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

const config = require('../config/config').globalConfig;
const nanoPromise = require('../util/nanoPromise');
const debug = require('../util/debug')('audit:actions');
const { open } = require('../connect');

const auditEnabled = !!config.auditActions;

let _globalNano = null;
let _db = null;

async function ensureNano() {
const newGlobalNano = await open();
if (_globalNano !== newGlobalNano) {
_db = newGlobalNano.db.use(config.auditActionsDb);
}
return _db;
}

async function auditAction(action, username, ip, meta) {
if (!auditEnabled) return;
debug('logAction', action, username, ip);
validateString('action', action);
validateString('username', username);
validateString('ip', ip);
const doc = {
action,
username,
ip,
date: new Date().toISOString()
};
if (meta) {
doc.meta = meta;
}
const db = await ensureNano();
await nanoPromise.insertDocument(db, doc);
}

async function auditLogin(username, success, provider, ctx) {
if (!auditEnabled) return;
const action = success ? 'login.success' : 'login.failed';
await auditAction(action, username, ctx.ip, { provider });
}

function validateString(name, value) {
if (typeof value !== 'string') {
throw new TypeError(`${name} must be a string`);
}
}

module.exports = {
auditAction,
auditLogin
};
4 changes: 4 additions & 0 deletions src/config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ module.exports = {
},
entryUnicity: 'byOwner', // can be byOwner or global

// Options related to audit logs
auditActions: false,
auditActionsDb: 'roc-audit-actions',

// Options for Zenodo publication
zenodo: false,
zenodoSandbox: false,
Expand Down
6 changes: 3 additions & 3 deletions src/connect.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict';

const nano = require('nano');
const nanoLib = require('nano');
const agentkeepalive = require('agentkeepalive');

const config = require('./config/config').globalConfig;
Expand Down Expand Up @@ -34,7 +34,7 @@ function open() {
async function getGlobalNano() {
debug.trace('renew CouchDB cookie');
if (config.url && config.username && config.password) {
let _nano = nano({
let _nano = nanoLib({
url: config.url,
requestDefaults: {
agent: nanoAgent
Expand All @@ -45,7 +45,7 @@ async function getGlobalNano() {
config.username,
config.password
);
return nano({
return nanoLib({
url: config.url,
cookie,
requestDefaults: {
Expand Down
34 changes: 34 additions & 0 deletions src/initCouch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';

const config = require('./config/config').globalConfig;
const { open } = require('./connect');
const loadCouch = require('./util/load');
const debug = require('./util/debug')('main:initCouch');
const nanoPromise = require('./util/nanoPromise');

async function initCouch() {
const nano = await open();
if (config.auditActions) {
await setupAuditActions(nano);
}
loadCouch();
}

async function setupAuditActions(nano) {
debug('setup audit actions');
const auditActionsDb = config.auditActionsDb;
// Check if database is accessible
try {
const dbExists = await nanoPromise.getDatabase(nano, auditActionsDb);
if (!dbExists) {
throw new Error(
`audit actions database does not exist: ${auditActionsDb}`
);
}
} catch (e) {
debug.error('failed to get audit actions database: %s', auditActionsDb);
throw e;
}
}

module.exports = initCouch;
10 changes: 7 additions & 3 deletions src/server/auth/couchdb/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

const LocalStrategy = require('passport-local').Strategy;

const request = require('../../../util/requestPromise');
const { auditLogin } = require('../../../audit/actions');
const couchUrl = require('../../../config/config').globalConfig.url;
const isEmail = require('../../../util/isEmail');
const request = require('../../../util/requestPromise');
const util = require('../../middleware/util');
const auth = require('../../middleware/auth');

Expand All @@ -20,9 +21,10 @@ exports.init = function (passport, router) {
new LocalStrategy(
{
usernameField: 'username',
passwordField: 'password'
passwordField: 'password',
passReqToCallback: true
},
function (username, password, done) {
function (req, username, password, done) {
(async function () {
if (!isEmail(username)) {
return done(null, false, 'username must be an email');
Expand All @@ -38,8 +40,10 @@ exports.init = function (passport, router) {

res = JSON.parse(res.body);
if (res.error) {
auditLogin(username, false, 'couchdb', req.ctx);
return done(null, false, res.reason);
}
auditLogin(username, true, 'couchdb', req.ctx);
return done(null, {
email: res.name,
provider: 'local'
Expand Down
11 changes: 8 additions & 3 deletions src/server/auth/facebook/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,24 @@
// }
const FacebookStrategy = require('passport-facebook');

const { auditLogin } = require('../../../audit/actions');

exports.init = function (passport, router, config) {
passport.use(
new FacebookStrategy(
{
clientID: config.appId,
clientSecret: config.appSecret,
callbackURL: config.publicAddress + config.callbackURL,
enableProof: false
enableProof: false,
passReqToCallback: true
},
function (accessToken, refreshToken, profile, done) {
function (req, accessToken, refreshToken, profile, done) {
const email = profile._json.email;
auditLogin(email, true, 'facebook', req.ctx);
done(null, {
provider: 'facebook',
email: profile._json.email
email
});
}
)
Expand Down
7 changes: 5 additions & 2 deletions src/server/auth/github/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

const GitHubStrategy = require('passport-github').Strategy;

const { auditLogin } = require('../../../audit/actions');
const request = require('../../../util/requestPromise');

exports.init = function (passport, router, config) {
Expand All @@ -53,9 +54,10 @@ exports.init = function (passport, router, config) {
{
clientID: config.clientID,
clientSecret: config.clientSecret,
callbackURL: config.publicAddress + config.callbackURL
callbackURL: config.publicAddress + config.callbackURL,
passReqToCallback: true
},
function (accessToken, refreshToken, profile, done) {
function (req, accessToken, refreshToken, profile, done) {
// Get the user's email
(async function () {
const answer = await request({
Expand All @@ -71,6 +73,7 @@ exports.init = function (passport, router, config) {
profile.email = answer[0].email;
}
if (email[0]) profile.email = email[0].email;
auditLogin(profile.email, true, 'github', req.ctx);
done(null, {
provider: 'github',
email: profile.email
Expand Down
8 changes: 6 additions & 2 deletions src/server/auth/google/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

const GoogleStrategy = require('passport-google-oauth20').Strategy;

const { auditLogin } = require('../../../audit/actions');

exports.init = function (passport, router, config, mainConfig) {
// todo we should be able to put a relative callbackURL (add proxy: true) but there is a bug in passport-oauth2
// with the generation of redirect_url. see https://github.com/jaredhanson/passport-oauth2/blob/master/lib/strategy.js#L136
Expand All @@ -40,13 +42,15 @@ exports.init = function (passport, router, config, mainConfig) {
{
clientID: config.clientID,
clientSecret: config.clientSecret,
callbackURL: `${mainConfig.publicAddress}/auth/login/google/callback`
callbackURL: `${mainConfig.publicAddress}/auth/login/google/callback`,
passReqToCallback: true
},
function (accessToken, refreshToken, profile, done) {
function (req, accessToken, refreshToken, profile, done) {
const email = profile.emails.find((email) => email.type === 'account');
if (!email) {
return done(null, false, { message: 'No account email' });
} else {
auditLogin(email.value, true, 'google', req.ctx);
done(null, {
provider: 'google',
email: email.value
Expand Down
6 changes: 5 additions & 1 deletion src/server/auth/ldap/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
// Doc: https://github.com/vesse/passport-ldapauth#readme
const LdapStrategy = require('passport-ldapauth');

const { auditLogin } = require('../../../audit/actions');
const util = require('../../middleware/util');
const auth = require('../../middleware/auth');

exports.init = function (passport, router, config) {
const strategyConfig = Object.assign({ passReqToCallback: true }, config);
passport.use(
new LdapStrategy(config, function (user, done) {
new LdapStrategy(strategyConfig, function (req, user, done) {
const data = {
provider: 'ldap',
email: user.mail,
Expand All @@ -26,11 +28,13 @@ exports.init = function (passport, router, config) {
return Promise.resolve(config.getUserInfo(user)).then(
(info) => {
data.info = info;
auditLogin(data.email, true, 'ldap', req.ctx);
done(null, data);
},
(err) => done(err)
);
} else {
auditLogin(data.email, true, 'ldap', req.ctx);
done(null, data);
return true;
}
Expand Down
17 changes: 13 additions & 4 deletions src/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const hbs = require('koa-hbs');

const config = require('../config/config').globalConfig;
const debug = require('../util/debug')('server');
const initCouch = require('../initCouch');

const api = require('./routes/api');
const auth = require('./routes/auth');
Expand Down Expand Up @@ -145,10 +146,18 @@ app.use(api.routes());

module.exports.start = function () {
if (_started) return _started;
_started = new Promise(function (resolve) {
http.createServer(app.callback()).listen(config.port, function () {
debug.warn(`running on localhost: ${config.port}`);
resolve(app);
_started = new Promise(function (resolve, reject) {
initCouch().then(() => {
http.createServer(app.callback()).listen(config.port, function () {
debug.warn(`running on localhost: ${config.port}`);
resolve(app);
});
}, (e) => {
reject(e);
process.nextTick(() => {
debug.error('initialization failed');
throw e;
});
});
});
return _started;
Expand Down
4 changes: 2 additions & 2 deletions src/util/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ const debug = require('./debug')('util:load');

var loaded = false;

module.exports = function () {
debug.trace('preload databases that have a confguration file');
module.exports = function loadCouch() {
debug.trace('preload databases that have a configuration file');
const homeDir = config.globalConfig.homeDir;
if (!homeDir) return;
if (loaded) return;
Expand Down

0 comments on commit 6d1406c

Please sign in to comment.