Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions api/controllers/agent/decide.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module.exports = {
fn: async function () {
const req = this.req;
const res = this.res;
if (!_.get(req, 'user.permissions.stock.host.update')) {
if (!sails.helpers.audit.can(req, 'host.update')) {
return res.forbidden();
}
const action = req.param('action');
Expand All @@ -34,11 +34,13 @@ module.exports = {
const by = req.user.username;
if (action === 'deny') {
await Enrollment.updateOne({id: row.id}).set({state: 'denied', cert: '', decidedAt: Date.now(), decidedBy: by, decidedVia: 'admin'});
// The 1.6 trail text (Enrollment::deny). The row is the admin's.
await sails.helpers.agent.audit.with({req, row, text: `denied by ${by}`, by});
return res.json({id: row.id, hostID: row.host, state: 'denied'});
}

try {
await sails.helpers.agent.issue.with({row, via: 'admin', by, keepCert: true});
await sails.helpers.agent.issue.with({row, via: 'admin', by, keepCert: true, req});
} catch (err) {
if (err.exit === 'notBound') { return res.status(409).json({error: 'enrollment is not bound to a host'}); }
return res.status(503).json({error: 'signing'});
Expand Down
18 changes: 13 additions & 5 deletions api/controllers/agent/enroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,16 @@ module.exports = {
not_after: result.notAfter
});
const signing = () => res.status(503).json({status: 'error', reason: 'signing'});
// Run the automatic approval; answer 200, 503 or (when it waits) 202.
const autoApprove = async (row) => {
// The 1.6 audit row of a transition (Enrollment::_audit), as the agent.
const audit = (row, what) => sails.helpers.agent.audit.with({req, row, text: what, by: ''});
// Run the automatic approval; answer 200, 503 or (when it waits) 202. On a
// first contact that waits, 1.6 records why (a repeat records nothing).
const autoApprove = async (row, firstContact = false) => {
try {
const result = await sails.helpers.agent.autoApprove.with({row, token: text(body.token, MAX_TOKEN)});
const result = await sails.helpers.agent.autoApprove.with({row, token: text(body.token, MAX_TOKEN), req});
if (!result && firstContact) {
await audit(row, row.reason === 'no-mac' ? 'waiting: no MAC address reported' : `waiting: ${row.reason}`);
}
return result ? issued(result) : pending(row);
} catch (unused) {
return signing();
Expand Down Expand Up @@ -148,7 +154,9 @@ module.exports = {
}
if (found.state === 'issued') {
// The agent lost its certificate but kept its key. An admin looks at it.
return pending(await Enrollment.updateOne({id: found.id}).set({state: 'pending', reason: 'reissue'}));
const again = await Enrollment.updateOne({id: found.id}).set({state: 'pending', reason: 'reissue'});
await audit(again, 'waiting: reissue');
return pending(again);
}
// Still pending: refresh the row, and re-check the automatic paths. A
// token may be valid now, or the deploy may have finished.
Expand Down Expand Up @@ -195,6 +203,6 @@ module.exports = {
});
row = await Enrollment.updateOne({id: row.id}).set({host: host.id});
}
return await autoApprove(row);
return await autoApprove(row, true);
}
};
2 changes: 1 addition & 1 deletion api/controllers/agent/enrollments.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ module.exports = {
description: 'List the pending agent enrollment requests.',
fn: async function () {
const res = this.res;
if (!_.get(this.req, 'user.permissions.stock.host.read')) {
if (!sails.helpers.audit.can(this.req, 'host.read')) {
return res.forbidden();
}
const rows = await sails.models.agentenrollment.find({state: 'pending'}).sort('createdAt ASC');
Expand Down
14 changes: 13 additions & 1 deletion api/controllers/agent/mint-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module.exports = {
fn: async function () {
const req = this.req;
const res = this.res;
if (!_.get(req, 'user.permissions.stock.host.create')) {
if (!sails.helpers.audit.can(req, 'host.create')) {
return res.forbidden();
}
const bad = (error) => res.status(400).json({error});
Expand All @@ -41,6 +41,18 @@ module.exports = {
expires,
createdBy: req.user.username
}).fetch();
// The 1.6 trail text (Token::mint). Never the token or its hash. `expires`
// is UTC "YYYY-MM-DD HH:MM:SS", as the not_after of the enroll answer is.
await sails.helpers.audit.record.with({req, row: {
type: 'agent.token',
subjectType: 'agentenrolltoken',
subjectId: row.id,
subjectLabel: name,
permission: 'host.create',
outcome: 'allowed',
renderable: true,
text: `minted, ${uses === -1 ? 'unlimited uses' : `${uses} use(s)`}, expires ${new Date(expires).toISOString().slice(0, 19).replace('T', ' ')}`
}});
return res.json({id: row.id, name, token, expires});
}
};
13 changes: 12 additions & 1 deletion api/controllers/agent/revoke-token.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@ module.exports = {
fn: async function () {
const req = this.req;
const res = this.res;
if (!_.get(req, 'user.permissions.stock.host.destroy')) {
if (!sails.helpers.audit.can(req, 'host.destroy')) {
return res.forbidden();
}
const row = await sails.models.agentenrolltoken.destroyOne({id: req.param('id')});
if (!row) { return res.status(404).json({error: 'no such token'}); }
// The 1.6 trail text (Token::revoke). The name survives in the row.
await sails.helpers.audit.record.with({req, row: {
type: 'agent.token',
subjectType: 'agentenrolltoken',
subjectId: row.id,
subjectLabel: row.name,
permission: 'host.destroy',
outcome: 'allowed',
renderable: true,
text: `revoked by ${req.user.username}`
}});
return res.json({id: row.id});
}
};
2 changes: 1 addition & 1 deletion api/controllers/agent/tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = {
description: 'List the agent enrollment tokens.',
fn: async function () {
const res = this.res;
if (!_.get(this.req, 'user.permissions.stock.host.read')) {
if (!sails.helpers.audit.can(this.req, 'host.read')) {
return res.forbidden();
}
const now = Date.now();
Expand Down
20 changes: 19 additions & 1 deletion api/controllers/auth/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@ module.exports = {
fn: async function (inputs, exits) {
let req = this.req;
let res = this.res;
// The attempted name for a refused sign-in row: trimmed, cut to the 191
// characters of a 1.6 user name.
const attempted = String(req.param('username') || '').trim().slice(0, 191);
// The auth.login.failed row. No reason is stored (1.6 stores none: it
// would help enumerate users). Only the outcome differs.
const recordFailed = (outcome) => sails.helpers.audit.record.with({req, row: {
type: 'auth.login.failed', outcome, subjectType: 'user', subjectId: '',
subjectLabel: attempted, createdBy: attempted, authSource: 'local', text: ''
}});
await passport.authenticate(sails.config.globals.authenticationMechanisms, async (err, user, info) => {
if (err) return exits.error(err);
if (err) {
// A strategy error (the database) is a failed sign-in, not a refused one.
await recordFailed('failed');
return exits.error(err);
}
// No user => bad credentials. Return a clean 401 (JSON) or bounce back to
// the login form, instead of calling req.login(undefined) which throws a
// 500 ("Failed to serialize user into session").
if (!user) {
await recordFailed('denied');
let message = (info && info.message) || 'Invalid username or password';
if (req.wantsJSON) {
res.status(401);
Expand All @@ -30,6 +44,10 @@ module.exports = {
}
await req.login(user, async (err) => {
if (err) return exits.error(err);
await sails.helpers.audit.record.with({req, row: {
type: 'auth.login', outcome: 'allowed', subjectType: 'user', subjectId: user.id,
subjectLabel: user.username, createdBy: user.username, authSource: 'local'
}});
await jwt.sign(
{user: user.id},
sails.config.auth.jwt.secret,
Expand Down
6 changes: 6 additions & 0 deletions api/controllers/auth/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ module.exports = {
fn: async function () {
let req = this.req;
let res = this.res;
// Record the sign-out BEFORE req.logout: after it the user is gone and the
// row would name nobody.
await sails.helpers.audit.record.with({req, row: {
type: 'auth.logout', outcome: 'allowed', subjectType: 'user',
subjectId: req.user.id, subjectLabel: req.user.username
}});
// passport >= 0.6 made req.logout() asynchronous -- it requires a callback
// and throws (500) if called the old synchronous way. Await it so the action
// doesn't return (and send a default response) before the redirect.
Expand Down
4 changes: 4 additions & 0 deletions api/controllers/general/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ module.exports = {
// a host's groups change that the caller may not make (403).
let check = await sails.helpers.group.checkSave.with({model, values: params, permissions: req.user.permissions});
if (check.error) {
// A refusal names the group permission that refused it (the audit trail).
if (check.error.permission) { _.set(req, 'audit.permission', check.error.permission); }
throw {[{400: 'badRequest', 403: 'forbidden', 409: 'conflict'}[check.error.status]]: {message: check.error.message}};
}
let obj = await sails.models[model].create(params)
Expand All @@ -45,6 +47,8 @@ module.exports = {
return {badRequest: err};
})
.fetch();
// The audit trail: the row that this request wrote (api/hooks/audit).
sails.helpers.audit.subject(req, model, obj);
return await sails.models[model].findOne({id: obj.id}).populateAll();
}
};
8 changes: 8 additions & 0 deletions api/controllers/general/destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ module.exports = {
id = [id];
}
let destroyed = await sails.models[model].destroy({id}).fetch();
// The audit trail (api/hooks/audit): one row for the request. One id names
// the row; several ids give the count, with no subject id.
if (id.length > 1) {
sails.helpers.audit.subject(req, model);
req.audit.affectedCount = destroyed.length;
} else {
sails.helpers.audit.subject(req, model, destroyed[0], id[0]);
}
// A destroyed Group leaves its members: the name goes from each host's
// groups, and no host is deleted.
if (model === 'group') {
Expand Down
17 changes: 15 additions & 2 deletions api/controllers/general/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ module.exports = {
// Permission: the generic create/update policies can't run here (no :model
// param), so check the relevant permission for the derived model directly.
let perm = isUpdate ? 'update' : 'create';
if (!_.get(req, 'user.permissions.stock.' + model + '.' + perm)) {
// The audit trail (api/hooks/audit): the permission before the decision,
// and the row once it is known.
sails.helpers.audit.subject(req, model, null, id);
if (!sails.helpers.audit.can(req, model + '.' + perm)) {
res.forbidden();
return res.end();
}
Expand Down Expand Up @@ -123,7 +126,11 @@ module.exports = {
model, id: isUpdate ? id : undefined, values, permissions: req.user.permissions
});
if (check.error) {
if (check.error.status === 403) { failStatus = 403; }
if (check.error.status === 403) {
failStatus = 403;
// The group permission that refused it (the audit trail).
_.set(req, 'audit.permission', check.error.permission);
}
throw new Error(check.error.message);
}
oldGroup = check.oldGroup;
Expand All @@ -133,13 +140,15 @@ module.exports = {
if (isUpdate) {
let saved = await sails.models[model].updateOne({ id }).set(values);
exists = !!saved;
if (saved) { sails.helpers.audit.subject(req, model, saved); }
// The members keep their membership through a rename, as in 1.6.
if (oldGroup && saved && saved.name !== oldGroup.name) {
await sails.helpers.group.rename.with({from: oldGroup.name, to: saved.name});
}
} else {
let created = await sails.models[model].create(values).fetch();
recordId = created.id;
sails.helpers.audit.subject(req, model, created);
}
// Apply many-to-many associations (replace == authoritative set).
for (let assoc of _.keys(collections)) {
Expand All @@ -161,6 +170,10 @@ module.exports = {
if (model === 'group' && err && err.code === 'E_UNIQUE') {
msg = isUpdate ? sails.models.group.takenText : sails.models.group.existsText;
}
// The audit trail: a full-page failure answers 302, which the seam
// would read as allowed. A refused group change is denied, any other
// error is failed.
req.audit.outcome = failStatus === 403 ? 'denied' : 'failed';
// Modal/AJAX submit -> JSON, and the modal shows the message itself;
// full-page submit -> back to the form, with the message as a toast on
// the next page (api/helpers/flash.js).
Expand Down
5 changes: 5 additions & 0 deletions api/controllers/general/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ module.exports = {
// A Group name that is not text (400) or is taken without case (409), or
// a host's groups change that the caller may not make (403). oldGroup is
// the Group before a rename.
// The audit trail names the row (api/hooks/audit); the label follows the write.
sails.helpers.audit.subject(req, model, null, id);
let check = await sails.helpers.group.checkSave.with({model, id, values: params, permissions: req.user.permissions});
if (check.error) {
// A refusal names the group permission that refused it (the audit trail).
if (check.error.permission) { _.set(req, 'audit.permission', check.error.permission); }
throw {[{400: 'badRequest', 403: 'forbidden', 409: 'conflict'}[check.error.status]]: {message: check.error.message}};
}
let oldGroup = check.oldGroup;
Expand All @@ -46,6 +50,7 @@ module.exports = {
.intercept({name: 'UsageError'}, (err) => {
return {badRequest: err};
});
if (obj) { sails.helpers.audit.subject(req, model, obj); }
// The members keep their membership through a rename, as in 1.6.
if (oldGroup && obj && obj.name !== oldGroup.name) {
await sails.helpers.group.rename.with({from: oldGroup.name, to: obj.name});
Expand Down
15 changes: 12 additions & 3 deletions api/controllers/host/bulk.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,21 @@ module.exports = {
// included.
let failTitle = (addGroups.length || !removeGroups.length) ? 'Add Hosts to Group Fail' : 'Remove Hosts from Groups Fail';

// The audit trail (api/hooks/audit): one row for the request. The
// permission goes in before each check, so a denial carries it. A
// membership change consults group.update; a refusal then sets the group
// permission that refused. The count follows the writes.
sails.helpers.audit.subject(req, 'host');
_.set(req, 'audit.permission', (hostFields || !membership) ? 'host.update' : 'group.update');
// A call that changes no membership keeps its host.update check.
if ((hostFields || !membership) && !_.get(permissions, 'stock.host.update')) {
if ((hostFields || !membership) && !sails.helpers.audit.can(req, 'host.update')) {
return res.forbidden();
}
if (membership) {
let denied = await sails.helpers.group.authorize.with({permissions, names: addGroups});
if (denied) {
return res.status(403).json({message: denied, title: failTitle});
req.audit.permission = denied.permission;
return res.status(403).json({message: denied.message, title: failTitle});
}
}

Expand Down Expand Up @@ -104,13 +111,15 @@ module.exports = {
let names = _.uniq(written, (n) => String(n).toLowerCase());
let denied = await sails.helpers.group.authorize.with({permissions, names});
if (denied) {
return res.status(403).json({message: denied, title: failTitle});
req.audit.permission = denied.permission;
return res.status(403).json({message: denied.message, title: failTitle});
}
}
for (let w of writes) {
await sails.models.host.updateOne({ id: w.id }).set(w.set);
updated++;
}
req.audit.affectedCount = updated;

// The 1.6 success toast (HostManagement.php ~6950-6970). A removal counts
// the named Groups that exist.
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/host/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = {
let req = this.req;
let res = this.res;

if (!_.get(req, 'user.permissions.stock.host.read')) {
if (!sails.helpers.audit.can(req, 'host.read')) {
return res.forbidden();
}

Expand Down
2 changes: 1 addition & 1 deletion api/controllers/host/pending-approve.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = {
fn: async function () {
const req = this.req;
const res = this.res;
if (!_.get(req, 'user.permissions.stock.host.update')) {
if (!sails.helpers.audit.can(req, 'host.update')) {
return res.forbidden();
}
const raw = _.get(req.body, 'ids');
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/host/pending-datatable.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ module.exports = {
fn: async function () {
const req = this.req;
const res = this.res;
if (!_.get(req, 'user.permissions.stock.host.read')) {
if (!sails.helpers.audit.can(req, 'host.read')) {
return res.forbidden();
}
const body = req.body || {};
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/host/pending-delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {
fn: async function () {
const req = this.req;
const res = this.res;
if (!_.get(req, 'user.permissions.stock.host.destroy')) {
if (!sails.helpers.audit.can(req, 'host.destroy')) {
return res.forbidden();
}
const raw = req.allParams().id;
Expand Down
3 changes: 1 addition & 2 deletions api/controllers/image/scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ module.exports = {
fn: async function () {
let req = this.req;
let res = this.res;
let perms = req.user && req.user.permissions && req.user.permissions.stock && req.user.permissions.stock.image;

// Creating image records, so require the image create permission.
if (!perms || !perms.create) { return res.forbidden(); }
if (!sails.helpers.audit.can(req, 'image.create')) { return res.forbidden(); }

return res.json(await sails.helpers.image.scanStore());
}
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/pages/list/agent-tokens.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module.exports = {
}
},
fn: async function () {
if (!_.get(this.req, 'user.permissions.stock.host.read')) {
if (!sails.helpers.audit.can(this.req, 'host.read')) {
throw 'forbidden';
}
let data = {
Expand Down
2 changes: 1 addition & 1 deletion api/controllers/pages/list/pending-agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module.exports = {
}
},
fn: async function () {
if (!_.get(this.req, 'user.permissions.stock.host.read')) {
if (!sails.helpers.audit.can(this.req, 'host.read')) {
throw 'forbidden';
}
let data = {
Expand Down
Loading
Loading