Audit Log items, backend stuff, help pages

This commit is contained in:
Jamie Curnow
2018-08-01 21:18:17 +10:00
parent a43c2d74bf
commit 66e25e315b
47 changed files with 936 additions and 134 deletions

View File

@ -1,19 +1,10 @@
'use strict';
const error = require('../lib/error');
const auditLogModel = require('../models/audit-log');
const internalAuditLog = {
/**
* Internal use only
*
* @param {Object} data
* @returns {Promise}
*/
create: data => {
// TODO
},
/**
* All logs
*
@ -28,16 +19,14 @@ const internalAuditLog = {
let query = auditLogModel
.query()
.orderBy('created_on', 'DESC')
.limit(100);
.limit(100)
.allowEager('[user]');
// Query is used for searching
if (typeof search_query === 'string') {
/*
query.where(function () {
this.where('name', 'like', '%' + search_query + '%')
.orWhere('email', 'like', '%' + search_query + '%');
this.where('meta', 'like', '%' + search_query + '%');
});
*/
}
if (typeof expand !== 'undefined' && expand !== null) {
@ -46,6 +35,44 @@ const internalAuditLog = {
return query;
});
},
/**
* This method should not be publicly used, it doesn't check certain things. It will be assumed
* that permission to add to audit log is already considered, however the access token is used for
* default user id determination.
*
* @param {Access} access
* @param {Object} data
* @param {String} data.action
* @param {Integer} [data.user_id]
* @param {Integer} [data.object_id]
* @param {Integer} [data.object_type]
* @param {Object} [data.meta]
* @returns {Promise}
*/
add: (access, data) => {
return new Promise((resolve, reject) => {
// Default the user id
if (typeof data.user_id === 'undefined' || !data.user_id) {
data.user_id = access.token.get('attrs').id;
}
if (typeof data.action === 'undefined' || !data.action) {
reject(new error.InternalValidationError('Audit log entry must contain an Action'));
} else {
// Make sure at least 1 of the IDs are set and action
resolve(auditLogModel
.query()
.insert({
user_id: data.user_id,
action: data.action,
object_type: data.object_type || '',
object_id: data.object_id || 0,
meta: data.meta || {}
}));
}
});
}
};

View File

@ -1,9 +1,10 @@
'use strict';
const _ = require('lodash');
const error = require('../lib/error');
const deadHostModel = require('../models/dead_host');
const internalHost = require('./host');
const _ = require('lodash');
const error = require('../lib/error');
const deadHostModel = require('../models/dead_host');
const internalHost = require('./host');
const internalAuditLog = require('./audit-log');
function omissions () {
return ['is_deleted'];
@ -49,7 +50,16 @@ const internalDeadHost = {
.insertAndFetch(data);
})
.then(row => {
return _.omit(row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(row, omissions());
});
});
},
@ -97,7 +107,17 @@ const internalDeadHost = {
.patchAndFetchById(row.id, data)
.then(saved_row => {
saved_row.meta = internalHost.cleanMeta(saved_row.meta);
return _.omit(saved_row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
});
},
@ -171,6 +191,17 @@ const internalDeadHost = {
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// Add to audit log
row.meta = internalHost.cleanMeta(row.meta);
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
@ -200,7 +231,15 @@ const internalDeadHost = {
});
})
.then(row => {
return _.pick(row.meta, internalHost.allowed_ssl_files);
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.pick(row.meta, internalHost.allowed_ssl_files);
});
});
},

View File

@ -0,0 +1,92 @@
'use strict';
const fs = require('fs');
const Liquid = require('liquidjs');
const logger = require('../logger').nginx;
const utils = require('../lib/utils');
const error = require('../lib/error');
const internalNginx = {
/**
* @returns {Promise}
*/
test: () => {
logger.info('Testing Nginx configuration');
return utils.exec('/usr/sbin/nginx -t');
},
/**
* @returns {Promise}
*/
reload: () => {
return internalNginx.test()
.then(() => {
logger.info('Reloading Nginx');
return utils.exec('/usr/sbin/nginx -s reload');
});
},
/**
* @param {String} host_type
* @param {Integer} host_id
* @returns {String}
*/
getConfigName: (host_type, host_id) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_');
return '/data/nginx/' + host_type + '/' + host_id + '.conf';
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
generateConfig: (host_type, host) => {
let renderEngine = Liquid();
host_type = host_type.replace(new RegExp('-', 'g'), '_');
return new Promise((resolve, reject) => {
let template = null;
let filename = internalNginx.getConfigName(host_type, host.id);
try {
template = fs.readFileSync(__dirname + '/../templates/' + host_type + '.conf', {encoding: 'utf8'});
} catch (err) {
reject(new error.ConfigurationError(err.message));
return;
}
return renderEngine
.parseAndRender(template, host)
.then(config_text => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
return true;
})
.catch(err => {
throw new error.ConfigurationError(err.message);
});
});
},
/**
* @param {String} host_type
* @param {Object} host
* @param {Boolean} [throw_errors]
* @returns {Promise}
*/
deleteConfig: (host_type, host, throw_errors) => {
return new Promise((resolve, reject) => {
try {
fs.unlinkSync(internalNginx.getConfigName(host_type, host.id));
} catch (err) {
if (throw_errors) {
reject(err);
}
}
resolve();
});
}
};
module.exports = internalNginx;

View File

@ -1,9 +1,10 @@
'use strict';
const _ = require('lodash');
const error = require('../lib/error');
const proxyHostModel = require('../models/proxy_host');
const internalHost = require('./host');
const _ = require('lodash');
const error = require('../lib/error');
const proxyHostModel = require('../models/proxy_host');
const internalHost = require('./host');
const internalAuditLog = require('./audit-log');
function omissions () {
return ['is_deleted'];
@ -49,7 +50,16 @@ const internalProxyHost = {
.insertAndFetch(data);
})
.then(row => {
return _.omit(row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(row, omissions());
});
});
},
@ -97,7 +107,17 @@ const internalProxyHost = {
.patchAndFetchById(row.id, data)
.then(saved_row => {
saved_row.meta = internalHost.cleanMeta(saved_row.meta);
return _.omit(saved_row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
});
},
@ -171,6 +191,17 @@ const internalProxyHost = {
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// Add to audit log
row.meta = internalHost.cleanMeta(row.meta);
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'proxy-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
@ -200,7 +231,15 @@ const internalProxyHost = {
});
})
.then(row => {
return _.pick(row.meta, internalHost.allowed_ssl_files);
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'proxy-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.pick(row.meta, internalHost.allowed_ssl_files);
});
});
},

View File

@ -4,6 +4,7 @@ const _ = require('lodash');
const error = require('../lib/error');
const redirectionHostModel = require('../models/redirection_host');
const internalHost = require('./host');
const internalAuditLog = require('./audit-log');
function omissions () {
return ['is_deleted'];
@ -49,7 +50,16 @@ const internalRedirectionHost = {
.insertAndFetch(data);
})
.then(row => {
return _.omit(row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(row, omissions());
});
});
},
@ -97,7 +107,17 @@ const internalRedirectionHost = {
.patchAndFetchById(row.id, data)
.then(saved_row => {
saved_row.meta = internalHost.cleanMeta(saved_row.meta);
return _.omit(saved_row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
});
},
@ -171,6 +191,17 @@ const internalRedirectionHost = {
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// Add to audit log
row.meta = internalHost.cleanMeta(row.meta);
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'redirection-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
@ -200,7 +231,15 @@ const internalRedirectionHost = {
});
})
.then(row => {
return _.pick(row.meta, internalHost.allowed_ssl_files);
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'redirection-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.pick(row.meta, internalHost.allowed_ssl_files);
});
});
},

163
src/backend/internal/ssl.js Normal file
View File

@ -0,0 +1,163 @@
'use strict';
const fs = require('fs');
const Liquid = require('liquidjs');
const timestamp = require('unix-timestamp');
const internalNginx = require('./nginx');
const logger = require('../logger').ssl;
const utils = require('../lib/utils');
const error = require('../lib/error');
timestamp.round = true;
const internalSsl = {
interval_timeout: 1000 * 60 * 60 * 12, // 12 hours
interval: null,
interval_processing: false,
initTimer: () => {
internalSsl.interval = setInterval(internalSsl.processExpiringHosts, internalSsl.interval_timeout);
},
/**
* Triggered by a timer, this will check for expiring hosts and renew their ssl certs if required
*/
processExpiringHosts: () => {
if (!internalSsl.interval_processing) {
logger.info('Renewing SSL certs close to expiry...');
return utils.exec('/usr/bin/certbot renew -q')
.then(result => {
logger.info(result);
internalSsl.interval_processing = false;
return internalNginx.reload()
.then(() => {
logger.info('Renew Complete');
return result;
});
})
.catch(err => {
logger.error(err);
internalSsl.interval_processing = false;
});
}
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Boolean}
*/
hasValidSslCerts: (host_type, host) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_');
let le_path = '/etc/letsencrypt/live/' + host_type + '_' + host.id;
return fs.existsSync(le_path + '/fullchain.pem') && fs.existsSync(le_path + '/privkey.pem');
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
requestSsl: (host_type, host) => {
logger.info('Requesting SSL certificates for ' + host_type + ' #' + host.id);
// TODO
return utils.exec('/usr/bin/letsencrypt certonly --agree-tos --email "' + host.letsencrypt_email + '" -n -a webroot -d "' + host.hostname + '"')
.then(result => {
logger.info(result);
return result;
});
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
renewSsl: (host_type, host) => {
logger.info('Renewing SSL certificates for ' + host_type + ' #' + host.id);
// TODO
return utils.exec('/usr/bin/certbot renew --force-renewal --disable-hook-validation --cert-name "' + host.hostname + '"')
.then(result => {
logger.info(result);
return result;
});
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
deleteCerts: (host_type, host) => {
logger.info('Deleting SSL certificates for ' + host_type + ' #' + host.id);
// TODO
return utils.exec('/usr/bin/certbot delete -n --cert-name "' + host.hostname + '"')
.then(result => {
logger.info(result);
})
.catch(err => {
logger.error(err);
});
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
generateSslSetupConfig: (host_type, host) => {
host_type = host_type.replace(new RegExp('-', 'g'), '_');
let renderEngine = Liquid();
let template = null;
let filename = internalNginx.getConfigName(host_type, host);
return new Promise((resolve, reject) => {
try {
template = fs.readFileSync(__dirname + '/../templates/letsencrypt.conf', {encoding: 'utf8'});
} catch (err) {
reject(new error.ConfigurationError(err.message));
return;
}
return renderEngine
.parseAndRender(template, host)
.then(config_text => {
fs.writeFileSync(filename, config_text, {encoding: 'utf8'});
return template_data;
})
.catch(err => {
throw new error.ConfigurationError(err.message);
});
});
},
/**
* @param {String} host_type
* @param {Object} host
* @returns {Promise}
*/
configureSsl: (host_type, host) => {
// TODO
return internalSsl.generateSslSetupConfig(host)
.then(data => {
return internalNginx.reload()
.then(() => {
return internalSsl.requestSsl(data);
});
});
}
};
module.exports = internalSsl;

View File

@ -1,8 +1,9 @@
'use strict';
const _ = require('lodash');
const error = require('../lib/error');
const streamModel = require('../models/stream');
const _ = require('lodash');
const error = require('../lib/error');
const streamModel = require('../models/stream');
const internalAuditLog = require('./audit-log');
function omissions () {
return ['is_deleted'];
@ -31,7 +32,16 @@ const internalStream = {
.insertAndFetch(data);
})
.then(row => {
return _.omit(row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'stream',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(row, omissions());
});
});
},
@ -60,7 +70,16 @@ const internalStream = {
.omit(omissions())
.patchAndFetchById(row.id, data)
.then(saved_row => {
return _.omit(saved_row, omissions());
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'stream',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
});
},
@ -133,6 +152,15 @@ const internalStream = {
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'stream',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {

View File

@ -7,6 +7,7 @@ const userPermissionModel = require('../models/user_permission');
const authModel = require('../models/auth');
const gravatar = require('gravatar');
const internalToken = require('./token');
const internalAuditLog = require('./audit-log');
function omissions () {
return ['is_deleted'];
@ -74,6 +75,18 @@ const internalUser = {
.then(() => {
return internalUser.get(access, {id: user.id, expand: ['permissions']});
});
})
.then(user => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'user',
object_id: user.id,
meta: user
})
.then(() => {
return user;
});
});
},
@ -136,6 +149,18 @@ const internalUser = {
})
.then(() => {
return internalUser.get(access, {id: data.id});
})
.then(user => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: data
})
.then(() => {
return user;
});
});
},
@ -236,6 +261,15 @@ const internalUser = {
.where('id', user.id)
.patch({
is_deleted: 1
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'user',
object_id: user.id,
meta: _.omit(user, omissions())
});
});
})
.then(() => {
@ -389,6 +423,19 @@ const internalUser = {
meta: {}
});
}
})
.then(() => {
// Add to Audit Log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: {
name: user.name,
password_changed: true,
auth_type: data.type
}
});
});
})
.then(() => {
@ -435,8 +482,21 @@ const internalUser = {
}
})
.then(permissions => {
return true;
// Add to Audit Log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'user',
object_id: user.id,
meta: {
name: user.name,
permissions: permissions
}
});
});
})
.then(() => {
return true;
});
},

View File

@ -50,6 +50,15 @@ module.exports = {
this.public = false;
},
ConfigurationError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.previous = previous;
this.message = message;
this.status = 400;
this.public = true;
},
CacheError: function (message, previous) {
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;

22
src/backend/lib/utils.js Normal file
View File

@ -0,0 +1,22 @@
'use strict';
const exec = require('child_process').exec;
module.exports = {
/**
* @param {String} cmd
* @returns {Promise}
*/
exec: function (cmd) {
return new Promise((resolve, reject) => {
exec(cmd, function (err, stdout, stderr) {
if (err && typeof err === 'object') {
reject(err);
} else {
resolve(stdout.trim());
}
});
});
}
};

View File

@ -1,8 +1,10 @@
const {Signale} = require('signale');
module.exports = {
global: new Signale({scope: 'Global '}),
migrate: new Signale({scope: 'Migrate '}),
express: new Signale({scope: 'Express '}),
access: new Signale({scope: 'Access '})
global: new Signale({scope: 'Global '}),
migrate: new Signale({scope: 'Migrate '}),
express: new Signale({scope: 'Express '}),
access: new Signale({scope: 'Access '}),
nginx: new Signale({scope: 'Nginx '}),
ssl: new Signale({scope: 'SSL '})
};

View File

@ -165,7 +165,8 @@ exports.up = function (knex/*, Promise*/) {
table.dateTime('created_on').notNull();
table.dateTime('modified_on').notNull();
table.integer('user_id').notNull().unsigned();
// TODO
table.string('object_type').notNull().defaultTo('');
table.integer('object_id').notNull().unsigned().defaultTo(0);
table.string('action').notNull();
table.json('meta').notNull();
});

View File

@ -5,6 +5,7 @@
const db = require('../db');
const Model = require('objection').Model;
const User = require('./user');
Model.knex(db);
@ -25,6 +26,26 @@ class AuditLog extends Model {
static get tableName () {
return 'audit_log';
}
static get jsonAttributes () {
return ['meta'];
}
static get relationMappings () {
return {
user: {
relation: Model.HasOneRelation,
modelClass: User,
join: {
from: 'audit_log.user_id',
to: 'user.id'
},
modify: function (qb) {
qb.omit(['id', 'created_on', 'modified_on', 'roles']);
}
}
};
}
}
module.exports = AuditLog;

View File

@ -0,0 +1,19 @@
# <%- hostname %>
server {
listen 80;
<%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %>
server_name <%- hostname %>;
access_log /config/logs/<%- hostname %>.log proxy;
<% if (typeof ssl !== 'undefined' && ssl) { -%>
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
<% } -%>
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
return 404;
}

View File

@ -0,0 +1,11 @@
# Letsencrypt Verification Temporary Host: <%- hostname %>
server {
listen 80;
server_name <%- hostname %>;
access_log /config/logs/letsencrypt.log proxy;
location / {
root /config/letsencrypt-acme-challenge;
}
}

View File

@ -0,0 +1,33 @@
# <%- hostname %>
server {
listen 80;
<%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %>
server_name <%- hostname %>;
access_log /config/logs/<%- hostname %>.log proxy;
set $server <%- forward_server %>;
set $port <%- forward_port %>;
<%- typeof asset_caching !== 'undefined' && asset_caching ? 'include conf.d/include/assets.conf;' : '' %>
<%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %>
<% if (typeof ssl !== 'undefined' && ssl) { -%>
include conf.d/include/letsencrypt-acme-challenge.conf;
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
<% } -%>
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
location / {
<% if (typeof access_list_id !== 'undefined' && access_list_id) { -%>
auth_basic "Authorization required";
auth_basic_user_file /config/access/<%- access_list_id %>;
<% } -%>
<%- typeof force_ssl !== 'undefined' && force_ssl ? 'include conf.d/include/force-ssl.conf;' : '' %>
include conf.d/include/proxy.conf;
}
}

View File

@ -0,0 +1,22 @@
# <%- hostname %>
server {
listen 80;
<%- typeof ssl !== 'undefined' && ssl ? 'listen 443 ssl;' : '' %>
server_name <%- hostname %>;
access_log /config/logs/<%- hostname %>.log proxy;
<%- typeof asset_caching !== 'undefined' && asset_caching ? 'include conf.d/include/assets.conf;' : '' %>
<%- typeof block_exploits !== 'undefined' && block_exploits ? 'include conf.d/include/block-exploits.conf;' : '' %>
<% if (typeof ssl !== 'undefined' && ssl) { -%>
include conf.d/include/ssl-ciphers.conf;
ssl_certificate /etc/letsencrypt/live/<%- hostname %>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<%- hostname %>/privkey.pem;
<% } -%>
<%- typeof advanced !== 'undefined' && advanced ? advanced : '' %>
return 301 $scheme://<%- forward_host %>$request_uri;
}

View File

@ -0,0 +1,11 @@
# <%- incoming_port %> - <%- protocols.join(',').toUpperCase() %>
<%
protocols.forEach(function (protocol) {
%>
server {
listen <%- incoming_port %> <%- protocol === 'tcp' ? '' : protocol %>;
proxy_pass <%- forward_server %>:<%- forward_port %>;
}
<%
});
%>