diff --git a/CHANGES b/CHANGES index 5ec6b1903..ee9ac15a6 100644 --- a/CHANGES +++ b/CHANGES @@ -2918,4 +2918,5 @@ * node: update to 22.13.1 * docker: update to 27.5.1 * s3: automatically abort old multipart uploads +* notifications: validate domains configs diff --git a/dashboard/public/js/client.js b/dashboard/public/js/client.js index 37ceb2624..e463ace26 100644 --- a/dashboard/public/js/client.js +++ b/dashboard/public/js/client.js @@ -18,6 +18,7 @@ const NOTIFICATION_TYPES = { APP_OOM: 'appOutOfMemory', APP_UPDATED: 'appUpdated', BACKUP_FAILED: 'backupFailed', + DOMAIN_CONFIG_CHECK_FAILED: 'domainConfigCheckFailed', }; // keep in sync with box/src/apps.js diff --git a/dashboard/public/js/index.js b/dashboard/public/js/index.js index 9bfdb66ea..dc1878e0b 100644 --- a/dashboard/public/js/index.js +++ b/dashboard/public/js/index.js @@ -127,6 +127,7 @@ app.filter('notificationTypeToColor', function () { case NOTIFICATION_TYPES.DISK_SPACE: case NOTIFICATION_TYPES.BACKUP_CONFIG: case NOTIFICATION_TYPES.BACKUP_FAILED: + case NOTIFICATION_TYPES.DOMAIN_CONFIG_CHECK_FAILED: return '#ff4c4c'; case NOTIFICATION_TYPES.BOX_UPDATE: case NOTIFICATION_TYPES.MANUAL_APP_UPDATE: diff --git a/src/cron.js b/src/cron.js index 9fd50b7cd..a6375eed9 100644 --- a/src/cron.js +++ b/src/cron.js @@ -29,6 +29,7 @@ const appHealthMonitor = require('./apphealthmonitor.js'), constants = require('./constants.js'), { CronJob } = require('cron'), debug = require('debug')('box:cron'), + domains = require('./domains.js'), dyndns = require('./dyndns.js'), externalLdap = require('./externalldap.js'), eventlog = require('./eventlog.js'), @@ -60,7 +61,8 @@ const gJobs = { schedulerSync: null, appHealthMonitor: null, diskUsage: null, - externalLdapSyncer: null + externalLdapSyncer: null, + checkDomainConfigs: null }; // cron format @@ -167,6 +169,13 @@ async function startJobs() { start: true }); + + gJobs.checkDomainConfigs = CronJob.from({ + cronTime: `00 ${minute} 5 * * *`, // once a day + onTick: async () => await safe(domains.checkConfigs(AuditSource.CRON), { debug }), + start: true + }); + gJobs.appHealthMonitor = CronJob.from({ cronTime: '*/10 * * * * *', // every 10 seconds onTick: async () => await safe(appHealthMonitor.run(10), { debug }), // 10 is the max run time diff --git a/src/domains.js b/src/domains.js index a860345ab..89064396f 100644 --- a/src/domains.js +++ b/src/domains.js @@ -13,6 +13,8 @@ module.exports = exports = { removePrivateFields, removeRestrictedFields, + + checkConfigs }; const assert = require('assert'), @@ -24,6 +26,7 @@ const assert = require('assert'), debug = require('debug')('box:domains'), eventlog = require('./eventlog.js'), mailServer = require('./mailserver.js'), + notifications = require('./notifications.js'), reverseProxy = require('./reverseproxy.js'), safe = require('safetydance'), tld = require('tldjs'), @@ -328,3 +331,36 @@ async function getDomainObjectMap() { for (const d of domainObjects) { domainObjectMap[d.domain] = d; } return domainObjectMap; } + +async function checkConfigs(auditSource) { + assert.strictEqual(typeof auditSource, 'object'); + + debug(`checkConfig: validating domain configs`); + + for (const domainObject of await list()) { + if (domainObject.provider === 'noop' || domainObject.provider === 'manual' || domainObject.provider === 'wildcard') { + await notifications.unpin(notifications.TYPE_DOMAIN_CONFIG_CHECK_FAILED, { context: domainObject.domain }); + continue; + } + + const [error] = await safe(api(domainObject.provider).verifyDomainConfig(domainObject)); + if (!error) { + await notifications.unpin(notifications.TYPE_DOMAIN_CONFIG_CHECK_FAILED, { context: domainObject.domain }); + continue; + } + + let errorMessage; + if (error.reason === BoxError.ACCESS_DENIED) { + errorMessage = `Access denied: ${error.message}`; + } else if (error.reason === BoxError.NOT_FOUND) { + errorMessage = `Zone not found: ${error.message}`; + } else if (error.reason === BoxError.EXTERNAL_ERROR) { + errorMessage = `Configuration error: ${error.message}`; + } else { + errorMessage = `General error: ${error.message}`; + } + + await notifications.pin(notifications.TYPE_DOMAIN_CONFIG_CHECK_FAILED, `Domain ${domainObject.domain} is not configured properly`, + errorMessage, { context: domainObject.domain }); + } +} diff --git a/src/notifications.js b/src/notifications.js index ddfdd864c..5e624ad8b 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -27,6 +27,7 @@ exports = module.exports = { TYPE_UPDATE_UBUNTU: 'ubuntuUpdate', TYPE_BOX_UPDATE: 'boxUpdate', TYPE_MANUAL_APP_UPDATE_NEEDED: 'manualAppUpdate', + TYPE_DOMAIN_CONFIG_CHECK_FAILED: 'domainConfigCheckFailed', // these work off singleton types pin, @@ -303,13 +304,6 @@ async function pin(type, title, message, options) { assert.strictEqual(typeof message, 'string'); assert.strictEqual(typeof options, 'object'); - // these are singletons. only one notification of such a type can be there - if (type !== exports.TYPE_DISK_SPACE && type !== exports.TYPE_MAIL_STATUS && type !== exports.TYPE_REBOOT && type !== exports.TYPE_UPDATE_UBUNTU && - type !== exports.TYPE_BOX_UPDATE && type !== exports.TYPE_MANUAL_APP_UPDATE_NEEDED) { - debug(`pin: notification of ${type} cannot be pinned`); - return null; - } - const result = await getByType(type, options.context || ''); if (!result) { await onPin(type);