diff --git a/src/apphealthmonitor.js b/src/apphealthmonitor.js index 5adf7ed78..ac5f63bb6 100644 --- a/src/apphealthmonitor.js +++ b/src/apphealthmonitor.js @@ -38,14 +38,14 @@ async function setHealth(app, health) { debug(`setHealth: ${app.id} (${app.fqdn}) switched from ${lastHealth} to healthy`); // do not send mails for dev apps - if (!app.debugMode) await eventlog.add(eventlog.ACTION_APP_UP, AuditSource.HEALTH_MONITOR, { app: app }); + if (!app.debugMode) await eventlog.add(eventlog.ACTION_APP_UP, AuditSource.HEALTH_MONITOR, { app }); } } else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) { if (lastHealth === apps.HEALTH_HEALTHY) { debug(`setHealth: marking ${app.id} (${app.fqdn}) as unhealthy since not seen for more than ${UNHEALTHY_THRESHOLD/(60 * 1000)} minutes`); // do not send mails for dev apps - if (!app.debugMode) await eventlog.add(eventlog.ACTION_APP_DOWN, AuditSource.HEALTH_MONITOR, { app: app }); + if (!app.debugMode) await eventlog.add(eventlog.ACTION_APP_DOWN, AuditSource.HEALTH_MONITOR, { app }); } } else { debug(`setHealth: ${app.id} (${app.fqdn}) waiting for ${(UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000} to update health`); diff --git a/src/auditsource.js b/src/auditsource.js index 79b3cdcc6..40eb59b7a 100644 --- a/src/auditsource.js +++ b/src/auditsource.js @@ -2,7 +2,7 @@ class AuditSource { constructor(username, userId, ip) { - this.username = username; + this.username = username || null; // this can be a real user or a module like cron/apptask/platform this.userId = userId || null; this.ip = ip || null; } @@ -11,14 +11,26 @@ class AuditSource { const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; return new AuditSource(req.user?.username, req.user?.id, ip); } + + static fromDirectoryServerRequest(req) { + const ip = req.connection.ldap.id; // also contains the port + return new AuditSource('directoryserver', null, ip); + } + + static fromOidcRequest(req) { + const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; + return new AuditSource('oidc', req.body?.username, ip); + } } // these can be static variables but see https://stackoverflow.com/questions/60046847/eslint-does-not-allow-static-class-properties#comment122122927_60464446 AuditSource.APPTASK = new AuditSource('apptask'); +AuditSource.BOOT = new AuditSource('boot'); AuditSource.CRON = new AuditSource('cron'); -AuditSource.EXTERNAL_LDAP_TASK = new AuditSource('externalldap'); -AuditSource.EXTERNAL_LDAP_AUTO_CREATE = new AuditSource('externalldap'); +AuditSource.EXTERNAL_LDAP = new AuditSource('externalldap'); AuditSource.HEALTH_MONITOR = new AuditSource('healthmonitor'); +AuditSource.LDAP = new AuditSource('ldap'); +AuditSource.MAIL = new AuditSource('mail'); AuditSource.PLATFORM = new AuditSource('platform'); exports = module.exports = AuditSource; diff --git a/src/directoryserver.js b/src/directoryserver.js index ef7c95316..d96f68873 100644 --- a/src/directoryserver.js +++ b/src/directoryserver.js @@ -11,6 +11,7 @@ exports = module.exports = { }; const assert = require('assert'), + AuditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:directoryserver'), @@ -363,7 +364,7 @@ async function start() { gServer.bind('ou=users,dc=cloudron', userAuth, async function (req, res) { assert.strictEqual(typeof req.user, 'object'); - await eventlog.upsertLoginEvent(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, { authType: 'directoryserver', id: req.connection.ldap.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + await eventlog.upsertLoginEvent(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, AuditSource.fromDirectoryServerRequest(req), { userId: req.user.id, user: users.removePrivateFields(req.user) }); res.end(); }); diff --git a/src/eventlog.js b/src/eventlog.js index 25e8bdbf4..97b8c330d 100644 --- a/src/eventlog.js +++ b/src/eventlog.js @@ -111,7 +111,7 @@ function postProcess(record) { // never throws, only logs because previously code did not take a callback async function add(action, source, data) { assert.strictEqual(typeof action, 'string'); - assert.strictEqual(typeof source, 'object'); + assert.strictEqual(typeof source, 'object'); // an AuditSource assert.strictEqual(typeof data, 'object'); const id = uuid.v4(); @@ -123,7 +123,7 @@ async function add(action, source, data) { // never throws, only logs because previously code did not take a callback async function upsertLoginEvent(action, source, data) { assert.strictEqual(typeof action, 'string'); - assert.strictEqual(typeof source, 'object'); + assert.strictEqual(typeof source, 'object'); // an AuditSource assert.strictEqual(typeof data, 'object'); // can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day diff --git a/src/externalldap.js b/src/externalldap.js index bb184aaa4..c67ac3923 100644 --- a/src/externalldap.js +++ b/src/externalldap.js @@ -275,7 +275,7 @@ async function maybeCreateUser(identifier) { const user = translateUser(config, ldapUsers[0]); if (!validUserRequirements(user)) throw new BoxError(BoxError.BAD_FIELD); - return await users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE); + return await users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP); } async function verifyPassword(user, password, totpToken) { @@ -346,17 +346,17 @@ async function syncUsers(config, progressCallback) { if (!user) { debug(`syncUsers: [adding user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`); - const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_TASK)); + const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP)); if (userAddError) debug('syncUsers: Failed to create user. %j %o', ldapUser, userAddError); } else if (user.source !== 'ldap') { debug(`syncUsers: [mapping user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`); - const [userMappingError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_TASK)); + const [userMappingError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP)); if (userMappingError) debug('Failed to map user. %j %o', ldapUser, userMappingError); } else if (user.email !== ldapUser.email || user.displayName !== ldapUser.displayName) { debug(`syncUsers: [updating user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`); - const [userUpdateError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName }, AuditSource.EXTERNAL_LDAP_TASK)); + const [userUpdateError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName }, AuditSource.EXTERNAL_LDAP)); if (userUpdateError) debug('Failed to update user. %j %o', ldapUser, userUpdateError); } else { // user known and up-to-date diff --git a/src/ldap.js b/src/ldap.js index aa819e2d6..0d6b2e87e 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -10,6 +10,7 @@ exports = module.exports = { const addonConfigs = require('./addonconfigs.js'), assert = require('assert'), apps = require('./apps.js'), + AuditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:ldap'), @@ -458,7 +459,7 @@ async function authorizeUserForApp(req, res, next) { // we return no such object, to avoid leakage of a users existence if (!canAccess) return next(new ldap.NoSuchObjectError(req.dn.toString())); - await eventlog.upsertLoginEvent(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) }); + await eventlog.upsertLoginEvent(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, AuditSource.LDAP, { appId: req.app.id, userId: req.user.id, user: users.removePrivateFields(req.user) }); res.end(); } @@ -603,7 +604,7 @@ async function authenticateService(serviceId, dn, req, res, next) { if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(dn.toString())); if (verifyError) return next(new ldap.OperationsError(verifyError.message)); - eventlog.upsertLoginEvent(result.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); + eventlog.upsertLoginEvent(result.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, AuditSource.MAIL, { mailboxId: email, userId: result.id, user: users.removePrivateFields(result) }); res.end(); } diff --git a/src/oidc.js b/src/oidc.js index ffd80f03d..5318ed33d 100644 --- a/src/oidc.js +++ b/src/oidc.js @@ -15,6 +15,7 @@ exports = module.exports = { const assert = require('assert'), apps = require('./apps.js'), + AuditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), blobs = require('./blobs.js'), branding = require('./branding.js'), @@ -510,7 +511,6 @@ function interactionLogin(provider) { const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; const userAgent = req.headers['user-agent'] || ''; - const auditSource = { authType: 'oidc', ip }; const clientId = details.params.client_id; debug(`interactionLogin: for OpenID client ${clientId} from ${ip}`); @@ -539,6 +539,7 @@ function interactionLogin(provider) { const [interactionFinishError, redirectTo] = await safe(provider.interactionResult(req, res, result)); if (interactionFinishError) return next(new HttpError(500, interactionFinishError)); + const auditSource = AuditSource.fromOidcRequest(req); await eventlog.add(user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: user.id, user: users.removePrivateFields(user), appId: clientId }); if (!user.ghost) safe(users.notifyLoginLocation(user, ip, userAgent, auditSource), { debug }); diff --git a/src/routes/auth.js b/src/routes/auth.js index 74978b264..91154edd7 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -32,7 +32,6 @@ async function login(req, res, next) { const type = req.body.type || tokens.ID_WEBADMIN; const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; const userAgent = req.headers['user-agent'] || ''; - const auditSource = { authType: 'basic', ip }; let error = tokens.validateTokenType(type); if (error) return next(new HttpError(400, error.message)); @@ -41,8 +40,8 @@ async function login(req, res, next) { [error, token] = await safe(tokens.add({ clientId: type, identifier: req.user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); if (error) return next(new HttpError(500, error)); - await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); - + const auditSource = AuditSource.fromRequest(req); + await eventlog.add(req.user.ghost ? eventlog.ACTION_USER_LOGIN_GHOST : eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user), type, appId: tokens.ID_CLI }); if (!req.user.ghost) safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource), { debug }); next(new HttpSuccess(200, token)); diff --git a/src/server.js b/src/server.js index 7a6dc6900..902f99446 100644 --- a/src/server.js +++ b/src/server.js @@ -6,6 +6,7 @@ exports = module.exports = { }; const assert = require('assert'), + AuditSource = require('./auditsource.js'), constants = require('./constants.js'), debug = require('debug')('box:server'), eventlog = require('./eventlog.js'), @@ -463,7 +464,7 @@ async function start() { gHttpServer = await initializeExpressSync(); await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.PORT, '127.0.0.1'); - await safe(eventlog.add(eventlog.ACTION_START, { userId: null, username: 'boot' }, { version: constants.VERSION })); // can fail if db down + await safe(eventlog.add(eventlog.ACTION_START, AuditSource.BOOT, { version: constants.VERSION })); // can fail if db down } async function stop() {