diff --git a/eslint.config.js b/eslint.config.js index 1a498a903..b40ce7d9f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -14,8 +14,7 @@ module.exports = [ }, rules: { semi: "error", - "prefer-const": "error", - "no-use-before-define": "error", + "prefer-const": "error" } } ]; diff --git a/src/apphealthmonitor.js b/src/apphealthmonitor.js index 2789ccc51..c96142c5a 100644 --- a/src/apphealthmonitor.js +++ b/src/apphealthmonitor.js @@ -1,5 +1,9 @@ 'use strict'; +exports = module.exports = { + run +}; + const apps = require('./apps.js'), assert = require('node:assert'), AuditSource = require('./auditsource.js'), @@ -11,10 +15,6 @@ const apps = require('./apps.js'), safe = require('safetydance'), superagent = require('@cloudron/superagent'); -exports = module.exports = { - run -}; - const UNHEALTHY_THRESHOLD = 20 * 60 * 1000; // 20 minutes const OOM_EVENT_LIMIT = 60 * 60 * 1000; // will only raise 1 oom event every hour diff --git a/src/backupformat/rsync.js b/src/backupformat/rsync.js index cdf0f9179..35142c229 100644 --- a/src/backupformat/rsync.js +++ b/src/backupformat/rsync.js @@ -1,5 +1,16 @@ 'use strict'; +exports = module.exports = { + download, + upload, + verify, + getFileExtension, + copy, + + _saveFsMetadata: saveFsMetadata, + _restoreFsMetadata: restoreFsMetadata +}; + const assert = require('node:assert'), async = require('async'), backupSites = require('../backupsites.js'), @@ -372,13 +383,3 @@ async function verify(backupSite, remotePath, integrityMap, progressCallback) { }; } -exports = module.exports = { - download, - upload, - verify, - getFileExtension, - copy, - - _saveFsMetadata: saveFsMetadata, - _restoreFsMetadata: restoreFsMetadata -}; diff --git a/src/backupintegrity.js b/src/backupintegrity.js index 09acdd0ed..48db50af8 100644 --- a/src/backupintegrity.js +++ b/src/backupintegrity.js @@ -1,5 +1,9 @@ 'use strict'; +exports = module.exports = { + check +}; + const backups = require('./backups.js'), backupFormats = require('./backupformats.js'), backupSites = require('./backupsites.js'), @@ -32,6 +36,3 @@ async function check(backupId, progressCallback) { }; } -exports = module.exports = { - check -}; diff --git a/src/backups.js b/src/backups.js index 6318e7e9f..f2be478b4 100644 --- a/src/backups.js +++ b/src/backups.js @@ -1,5 +1,33 @@ 'use strict'; +exports = module.exports = { + get, + getByIdentifierAndStatePaged, + getLatestInTargetByIdentifier, // brutal function name + add, + update, + setState, + list, + listBySiteId, + listByTypePaged, + del, + + removePrivateFields, + + startIntegrityCheck, + + BACKUP_IDENTIFIER_BOX: 'box', + BACKUP_IDENTIFIER_MAIL: 'mail', + + BACKUP_TYPE_APP: 'app', + BACKUP_TYPE_BOX: 'box', + BACKUP_TYPE_MAIL: 'mail', + + BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI? + BACKUP_STATE_CREATING: 'creating', + BACKUP_STATE_ERROR: 'error', +}; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), database = require('./database.js'), @@ -195,30 +223,3 @@ async function startIntegrityCheck(backup) { return taskId; } -exports = module.exports = { - get, - getByIdentifierAndStatePaged, - getLatestInTargetByIdentifier, // brutal function name - add, - update, - setState, - list, - listBySiteId, - listByTypePaged, - del, - - removePrivateFields, - - startIntegrityCheck, - - BACKUP_IDENTIFIER_BOX: 'box', - BACKUP_IDENTIFIER_MAIL: 'mail', - - BACKUP_TYPE_APP: 'app', - BACKUP_TYPE_BOX: 'box', - BACKUP_TYPE_MAIL: 'mail', - - BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI? - BACKUP_STATE_CREATING: 'creating', - BACKUP_STATE_ERROR: 'error', -}; diff --git a/src/backuptask.js b/src/backuptask.js index 27aaa3e04..15579b190 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -1,5 +1,19 @@ 'use strict'; +exports = module.exports = { + fullBackup, + appBackup, + + restore, + + downloadApp, + backupApp, + + downloadMail, + + upload, +}; + const apps = require('./apps.js'), assert = require('node:assert'), backupFormats = require('./backupformats.js'), @@ -557,16 +571,3 @@ async function appBackup(appId, backupSiteId, options, progressCallback) { return backupId; } -exports = module.exports = { - fullBackup, - appBackup, - - restore, - - downloadApp, - backupApp, - - downloadMail, - - upload, -}; diff --git a/src/changelog.js b/src/changelog.js index 082387053..3bd4b897c 100644 --- a/src/changelog.js +++ b/src/changelog.js @@ -1,13 +1,13 @@ 'use strict'; -const assert = require('node:assert'), - fs = require('node:fs'), - path = require('node:path'); - exports = module.exports = { getChanges }; +const assert = require('node:assert'), + fs = require('node:fs'), + path = require('node:path'); + function getChanges(version) { assert.strictEqual(typeof version, 'string'); diff --git a/src/dns.js b/src/dns.js index c783a00fb..4d98d757d 100644 --- a/src/dns.js +++ b/src/dns.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = exports = { +exports = module.exports = { fqdn, getName, diff --git a/src/dns/bunny.js b/src/dns/bunny.js index 9bf6d06c4..35a5b2fbc 100644 --- a/src/dns/bunny.js +++ b/src/dns/bunny.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -246,12 +256,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/cloudflare.js b/src/dns/cloudflare.js index fc92161a5..eba49e3df 100644 --- a/src/dns/cloudflare.js +++ b/src/dns/cloudflare.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -282,12 +292,3 @@ async function verifyDomainConfig(domainObject) { return sanitizedConfig; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/desec.js b/src/dns/desec.js index 8ed26debc..185d07fc4 100644 --- a/src/dns/desec.js +++ b/src/dns/desec.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), constants = require('../constants.js'), BoxError = require('../boxerror.js'), @@ -157,12 +167,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/digitalocean.js b/src/dns/digitalocean.js index 515ae2693..d857905d6 100644 --- a/src/dns/digitalocean.js +++ b/src/dns/digitalocean.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -243,12 +253,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/dnsimple.js b/src/dns/dnsimple.js index 3b855d114..2630c8c09 100644 --- a/src/dns/dnsimple.js +++ b/src/dns/dnsimple.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -255,12 +265,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/gandi.js b/src/dns/gandi.js index 782895b76..dce45586a 100644 --- a/src/dns/gandi.js +++ b/src/dns/gandi.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -162,12 +172,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/gcdns.js b/src/dns/gcdns.js index 1fa20ff4b..52473e7ef 100644 --- a/src/dns/gcdns.js +++ b/src/dns/gcdns.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -183,12 +193,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/godaddy.js b/src/dns/godaddy.js index fc7516761..6181a9f83 100644 --- a/src/dns/godaddy.js +++ b/src/dns/godaddy.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -183,12 +193,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/hetzner.js b/src/dns/hetzner.js index edf6e3ba3..e3afa2805 100644 --- a/src/dns/hetzner.js +++ b/src/dns/hetzner.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -249,12 +259,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/inwx.js b/src/dns/inwx.js index 09caf99a2..aa8a20d31 100644 --- a/src/dns/inwx.js +++ b/src/dns/inwx.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const { ApiClient, Language } = require('domrobot-client'), assert = require('node:assert'), BoxError = require('../boxerror.js'), @@ -208,12 +218,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/linode.js b/src/dns/linode.js index 87b501151..94f344d43 100644 --- a/src/dns/linode.js +++ b/src/dns/linode.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), constants = require('../constants.js'), BoxError = require('../boxerror.js'), @@ -258,12 +268,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/namecheap.js b/src/dns/namecheap.js index 4c514de10..212172417 100644 --- a/src/dns/namecheap.js +++ b/src/dns/namecheap.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + verifyDomainConfig, + wait +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -275,12 +285,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - verifyDomainConfig, - wait -}; diff --git a/src/dns/namecom.js b/src/dns/namecom.js index d60b50dd1..44b797348 100644 --- a/src/dns/namecom.js +++ b/src/dns/namecom.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -241,12 +251,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/netcup.js b/src/dns/netcup.js index 6f70db011..11bce2572 100644 --- a/src/dns/netcup.js +++ b/src/dns/netcup.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -253,12 +263,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/ovh.js b/src/dns/ovh.js index cd42a3975..fb1b06754 100644 --- a/src/dns/ovh.js +++ b/src/dns/ovh.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -225,12 +235,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/porkbun.js b/src/dns/porkbun.js index 3a4ec75fb..53863292b 100644 --- a/src/dns/porkbun.js +++ b/src/dns/porkbun.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), constants = require('../constants.js'), @@ -217,12 +227,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dns/route53.js b/src/dns/route53.js index be78da39b..2b4876f86 100644 --- a/src/dns/route53.js +++ b/src/dns/route53.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig, +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), { ConfiguredRetryStrategy } = require('@smithy/util-retry'), @@ -261,12 +271,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig, -}; diff --git a/src/dns/vultr.js b/src/dns/vultr.js index b6a2beb3a..a5640209f 100644 --- a/src/dns/vultr.js +++ b/src/dns/vultr.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + injectPrivateFields, + upsert, + get, + del, + wait, + verifyDomainConfig +}; + const assert = require('node:assert'), constants = require('../constants.js'), BoxError = require('../boxerror.js'), @@ -228,12 +238,3 @@ async function verifyDomainConfig(domainObject) { return credentials; } -exports = module.exports = { - removePrivateFields, - injectPrivateFields, - upsert, - get, - del, - wait, - verifyDomainConfig -}; diff --git a/src/dockerregistries.js b/src/dockerregistries.js index 98d6e4e37..c451d2ee3 100644 --- a/src/dockerregistries.js +++ b/src/dockerregistries.js @@ -1,5 +1,15 @@ 'use strict'; +exports = module.exports = { + removePrivateFields, + + list, + add, + get, + del, + update, +}; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), @@ -111,12 +121,3 @@ async function del(registry, auditSource) { await eventlog.add(eventlog.ACTION_REGISTRY_DEL, auditSource, { registry: removePrivateFields(registry) }); } -exports = module.exports = { - removePrivateFields, - - list, - add, - get, - del, - update, -}; diff --git a/src/domains.js b/src/domains.js index d305f14a1..e5d5abec9 100644 --- a/src/domains.js +++ b/src/domains.js @@ -1,6 +1,6 @@ 'use strict'; -module.exports = exports = { +exports = module.exports = { add, get, list, diff --git a/src/externalldap.js b/src/externalldap.js index 8e41800fd..b17dd88c2 100644 --- a/src/externalldap.js +++ b/src/externalldap.js @@ -1,5 +1,21 @@ 'use strict'; +exports = module.exports = { + getConfig, + setConfig, + + verifyPassword, + maybeCreateUser, + + supports2FA, + + startSyncer, + + removePrivateFields, + + sync +}; + const assert = require('node:assert'), AuditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), @@ -523,18 +539,3 @@ async function sync(progressCallback) { debug('sync: done'); } -exports = module.exports = { - getConfig, - setConfig, - - verifyPassword, - maybeCreateUser, - - supports2FA, - - startSyncer, - - removePrivateFields, - - sync -}; diff --git a/src/janitor.js b/src/janitor.js index 4b75a95b4..47d2fe259 100644 --- a/src/janitor.js +++ b/src/janitor.js @@ -1,5 +1,10 @@ 'use strict'; +exports = module.exports = { + cleanupTokens, + cleanupDockerVolumes +}; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), debug = require('debug')('box:janitor'), @@ -7,11 +12,6 @@ const assert = require('node:assert'), safe = require('safetydance'), tokens = require('./tokens.js'); -exports = module.exports = { - cleanupTokens, - cleanupDockerVolumes -}; - const gConnection = new Docker({ socketPath: '/var/run/docker.sock' }); async function cleanupTokens() { diff --git a/src/mail.js b/src/mail.js index 8c76f37bb..3e5b585b6 100644 --- a/src/mail.js +++ b/src/mail.js @@ -1,5 +1,65 @@ 'use strict'; +exports = module.exports = { + getStatus, + checkConfiguration, + + listDomains, + + getDomain, + clearDomains, + + removePrivateFields, + + setDnsRecords, + upsertDnsRecords, + + validateName, + validateDisplayName, + + setMailFromValidation, + setCatchAllAddress, + setMailRelay, + setMailEnabled, + setBanner, + + sendTestMail, + + getMailboxCount, + listMailboxes, + listAllMailboxes, + getMailbox, + addMailbox, + updateMailbox, + delMailbox, + + getAlias, + getAliases, + setAliases, + searchAlias, + + getListCount, + getLists, + getList, + addList, + updateList, + delList, + resolveList, + + checkStatus, + + OWNERTYPE_USER: 'user', + OWNERTYPE_GROUP: 'group', + OWNERTYPE_APP: 'app', + + TYPE_MAILBOX: 'mailbox', + TYPE_LIST: 'list', + TYPE_ALIAS: 'alias', + + _delByDomain: delByDomain, + _updateDomain: updateDomain +}; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), @@ -27,8 +87,6 @@ const assert = require('node:assert'), const DNS_OPTIONS = { timeout: 20000, tries: 4 }; const REMOVE_MAILBOX_CMD = path.join(__dirname, 'scripts/rmmailbox.sh'); -const OWNERTYPES = [ exports.OWNERTYPE_USER, exports.OWNERTYPE_GROUP, exports.OWNERTYPE_APP ]; - // if you add a field here, listMailboxes has to be updated const MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active', 'enablePop3', 'storageQuota', 'messagesQuota' ].join(','); const MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimKeyJson', 'dkimSelector', 'bannerJson' ].join(','); @@ -111,6 +169,11 @@ function validateDisplayName(name) { return null; } +function validateOwnerType(type) { + const OWNERTYPES = [ exports.OWNERTYPE_USER, exports.OWNERTYPE_GROUP, exports.OWNERTYPE_APP ]; + return OWNERTYPES.includes(type); +} + async function getDomain(domain) { assert.strictEqual(typeof domain, 'string'); @@ -862,7 +925,7 @@ async function addMailbox(name, domain, data, auditSource) { let error = validateName(name); if (error) throw error; - if (!OWNERTYPES.includes(ownerType)) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type'); + if (!validateOwnerType(ownerType)) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type'); [error] = await safe(database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active, storageQuota, messagesQuota, enablePop3 ])); @@ -888,7 +951,7 @@ async function updateMailbox(name, domain, data, auditSource) { continue; } - if (k === 'ownerType' && !OWNERTYPES.includes(data[k])) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type'); + if (k === 'ownerType' && !validateOwnerType(data[k])) throw new BoxError(BoxError.BAD_FIELD, 'bad owner type'); fields.push(k + ' = ?'); args.push(data[k]); @@ -1177,62 +1240,3 @@ async function checkStatus() { } } -exports = module.exports = { - getStatus, - checkConfiguration, - - listDomains, - - getDomain, - clearDomains, - - removePrivateFields, - - setDnsRecords, - upsertDnsRecords, - - validateName, - validateDisplayName, - - setMailFromValidation, - setCatchAllAddress, - setMailRelay, - setMailEnabled, - setBanner, - - sendTestMail, - - getMailboxCount, - listMailboxes, - listAllMailboxes, - getMailbox, - addMailbox, - updateMailbox, - delMailbox, - - getAlias, - getAliases, - setAliases, - searchAlias, - - getListCount, - getLists, - getList, - addList, - updateList, - delList, - resolveList, - - checkStatus, - - OWNERTYPE_USER: 'user', - OWNERTYPE_GROUP: 'group', - OWNERTYPE_APP: 'app', - - TYPE_MAILBOX: 'mailbox', - TYPE_LIST: 'list', - TYPE_ALIAS: 'alias', - - _delByDomain: delByDomain, - _updateDomain: updateDomain -}; diff --git a/src/mailserver.js b/src/mailserver.js index 9ff12eb94..010d98c90 100644 --- a/src/mailserver.js +++ b/src/mailserver.js @@ -200,6 +200,13 @@ async function configureMail(mailFqdn, mailDomain, serviceConfig) { await shell.bash(runCmd, { encoding: 'utf8' }); } +async function getLocation() { + const subdomain = await settings.get(settings.MAIL_SUBDOMAIN_KEY); + const domain = await settings.get(settings.MAIL_DOMAIN_KEY); + + return new Location(subdomain, domain, Location.TYPE_MAIL); +} + async function restart() { if (constants.TEST && !process.env.TEST_CREATE_INFRA) return; @@ -265,13 +272,6 @@ async function checkCertificate() { await restartIfActivated(); } -async function getLocation() { - const subdomain = await settings.get(settings.MAIL_SUBDOMAIN_KEY); - const domain = await settings.get(settings.MAIL_DOMAIN_KEY); - - return new Location(subdomain, domain, Location.TYPE_MAIL); -} - async function changeLocation(auditSource, progressCallback) { assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof progressCallback, 'function'); @@ -351,3 +351,4 @@ async function getMailAuth() { relayToken }; } + diff --git a/src/routes/volumes.js b/src/routes/volumes.js index 22ef0f56e..4f9e14a1d 100644 --- a/src/routes/volumes.js +++ b/src/routes/volumes.js @@ -1,5 +1,16 @@ 'use strict'; +exports = module.exports = { + add, + get, + update, + del, + list, + load, + remount, + getStatus +}; + const assert = require('node:assert'), AuditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), @@ -84,13 +95,3 @@ async function getStatus(req, res, next) { next(new HttpSuccess(200, status)); } -exports = module.exports = { - add, - get, - update, - del, - list, - load, - remount, - getStatus -}; diff --git a/src/shell.js b/src/shell.js index a2a361dfb..4c4fc7fe0 100644 --- a/src/shell.js +++ b/src/shell.js @@ -1,5 +1,7 @@ 'use strict'; +exports = module.exports = shell; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), child_process = require('node:child_process'), @@ -8,8 +10,6 @@ const assert = require('node:assert'), safe = require('safetydance'), _ = require('./underscore.js'); -exports = module.exports = shell; - function shell(tag) { assert.strictEqual(typeof tag, 'string'); diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js index e4e05691b..109c7fbfd 100644 --- a/src/storage/filesystem.js +++ b/src/storage/filesystem.js @@ -1,5 +1,30 @@ 'use strict'; +exports = module.exports = { + setup, + teardown, + cleanup, + + verifyConfig, + removePrivateFields, + injectPrivateFields, + + getAvailableSize, + getStatus, + + upload, + download, + + copy, + copyDir, + + exists, + listDir, + + remove, + removeDir, +}; + const assert = require('node:assert'), BoxError = require('../boxerror.js'), debug = require('debug')('box:storage/filesystem'), @@ -394,27 +419,3 @@ function injectPrivateFields(newConfig, currentConfig) { if (currentConfig._managedMountPath) newConfig._managedMountPath = currentConfig._managedMountPath; } -exports = module.exports = { - setup, - teardown, - cleanup, - - verifyConfig, - removePrivateFields, - injectPrivateFields, - - getAvailableSize, - getStatus, - - upload, - download, - - copy, - copyDir, - - exists, - listDir, - - remove, - removeDir, -}; diff --git a/src/storage/gcs.js b/src/storage/gcs.js index 43d0a36c9..82186ec6c 100644 --- a/src/storage/gcs.js +++ b/src/storage/gcs.js @@ -1,5 +1,30 @@ 'use strict'; +exports = module.exports = { + getAvailableSize, + getStatus, + + upload, + exists, + download, + + copy, + copyDir, + + listDir, + + remove, + removeDir, + + setup, + teardown, + cleanup, + + verifyConfig, + removePrivateFields, + injectPrivateFields, +}; + const assert = require('node:assert'), async = require('async'), BoxError = require('../boxerror.js'), @@ -258,27 +283,3 @@ function injectPrivateFields(newConfig, currentConfig) { if (!Object.hasOwn(newConfig.credentials, 'private_key') && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key; } -exports = module.exports = { - getAvailableSize, - getStatus, - - upload, - exists, - download, - - copy, - copyDir, - - listDir, - - remove, - removeDir, - - setup, - teardown, - cleanup, - - verifyConfig, - removePrivateFields, - injectPrivateFields, -}; diff --git a/src/storage/s3.js b/src/storage/s3.js index b6f37536c..1bcc83605 100644 --- a/src/storage/s3.js +++ b/src/storage/s3.js @@ -1,5 +1,32 @@ 'use strict'; +exports = module.exports = { + setup, + teardown, + cleanup, + + verifyConfig, + removePrivateFields, + injectPrivateFields, + + getAvailableSize, + getStatus, + + upload, + exists, + download, + copy, + copyDir, + + listDir, + + remove, + removeDir, + + // Used to mock AWS + _chunk: chunk +}; + const assert = require('node:assert'), async = require('async'), BoxError = require('../boxerror.js'), @@ -662,29 +689,3 @@ function injectPrivateFields(newConfig, currentConfig) { newConfig._provider = currentConfig._provider; } -exports = module.exports = { - setup, - teardown, - cleanup, - - verifyConfig, - removePrivateFields, - injectPrivateFields, - - getAvailableSize, - getStatus, - - upload, - exists, - download, - copy, - copyDir, - - listDir, - - remove, - removeDir, - - // Used to mock AWS - _chunk: chunk -}; diff --git a/src/syncer.js b/src/syncer.js index b9d728c89..eb0c189f9 100644 --- a/src/syncer.js +++ b/src/syncer.js @@ -1,5 +1,10 @@ 'use strict'; +exports = module.exports = { + sync, + finalize +}; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), DataLayout = require('./datalayout.js'), @@ -10,11 +15,6 @@ const assert = require('node:assert'), safe = require('safetydance'), util = require('node:util'); -exports = module.exports = { - sync, - finalize -}; - function readCache(cacheFile) { assert.strictEqual(typeof cacheFile, 'string'); diff --git a/src/tasks.js b/src/tasks.js index 256bf5139..943dc31bc 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -1,5 +1,54 @@ 'use strict'; +exports = module.exports = { + get, + add, + update, + setCompleted, + setCompletedByType, + list, + + getLogs, + + startTask, + stopTask, + stopAllTasks, + + removePrivateFields, + + _del: del, + + // task types. if you add a task here, fill up the function table in taskworker and dashboard constants.js + // '_' prefix is removed for lookup + TASK_APP: 'app', + + // "prefix" allows us to locate the tasks of a specific app or backup site + TASK_APP_BACKUP_PREFIX: 'appBackup_', + TASK_FULL_BACKUP_PREFIX: 'backup_', // full backup + TASK_CLEAN_BACKUPS_PREFIX: 'cleanBackups_', + + TASK_BOX_UPDATE: 'boxUpdate', + TASK_CHECK_CERTS: 'checkCerts', + TASK_SYNC_DYNDNS: 'syncDyndns', + TASK_PREPARE_DASHBOARD_LOCATION: 'prepareDashboardLocation', + TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap', + TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation', + TASK_SYNC_DNS_RECORDS: 'syncDnsRecords', + + TASK_CHECK_BACKUP_INTEGRITY: 'checkBackupIntegrity', + + // error codes + ESTOPPED: 'stopped', + ECRASHED: 'crashed', + ETIMEOUT: 'timeout', + + // testing + _TASK_IDENTITY: 'identity', + _TASK_CRASH: 'crash', + _TASK_ERROR: 'error', + _TASK_SLEEP: 'sleep' +}; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), database = require('./database.js'), @@ -243,51 +292,3 @@ async function del(id) { if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Task not found'); } -exports = module.exports = { - get, - add, - update, - setCompleted, - setCompletedByType, - list, - - getLogs, - - startTask, - stopTask, - stopAllTasks, - - removePrivateFields, - - _del: del, - - // task types. if you add a task here, fill up the function table in taskworker and dashboard constants.js - // '_' prefix is removed for lookup - TASK_APP: 'app', - - // "prefix" allows us to locate the tasks of a specific app or backup site - TASK_APP_BACKUP_PREFIX: 'appBackup_', - TASK_FULL_BACKUP_PREFIX: 'backup_', // full backup - TASK_CLEAN_BACKUPS_PREFIX: 'cleanBackups_', - - TASK_BOX_UPDATE: 'boxUpdate', - TASK_CHECK_CERTS: 'checkCerts', - TASK_SYNC_DYNDNS: 'syncDyndns', - TASK_PREPARE_DASHBOARD_LOCATION: 'prepareDashboardLocation', - TASK_SYNC_EXTERNAL_LDAP: 'syncExternalLdap', - TASK_CHANGE_MAIL_LOCATION: 'changeMailLocation', - TASK_SYNC_DNS_RECORDS: 'syncDnsRecords', - - TASK_CHECK_BACKUP_INTEGRITY: 'checkBackupIntegrity', - - // error codes - ESTOPPED: 'stopped', - ECRASHED: 'crashed', - ETIMEOUT: 'timeout', - - // testing - _TASK_IDENTITY: 'identity', - _TASK_CRASH: 'crash', - _TASK_ERROR: 'error', - _TASK_SLEEP: 'sleep' -}; diff --git a/src/users.js b/src/users.js index f790a285c..9da7a96b9 100644 --- a/src/users.js +++ b/src/users.js @@ -68,14 +68,6 @@ exports = module.exports = { compareRoles, }; -const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ]; - -// the avatar and backgroundImage fields are special and not added here to reduce response sizes -const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language', - 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson' ].join(','); - -const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours - const appPasswords = require('./apppasswords.js'), assert = require('node:assert'), BoxError = require('./boxerror.js'), @@ -105,6 +97,12 @@ const appPasswords = require('./apppasswords.js'), validator = require('./validator.js'), _ = require('./underscore.js'); +// the avatar and backgroundImage fields are special and not added here to reduce response sizes +const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language', + 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson' ].join(','); + +const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours + const CRYPTO_SALT_SIZE = 64; // 512-bit salt const CRYPTO_ITERATIONS = 10000; // iterations const CRYPTO_KEY_LENGTH = 512; // bits @@ -201,6 +199,28 @@ function removePrivateFields(user) { return result; } +function validateRole(role) { + assert.strictEqual(typeof role, 'string'); + + const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ]; + + if (ORDERED_ROLES.includes(role)) return null; + + return new BoxError(BoxError.BAD_FIELD, `Invalid role '${role}'`); +} + +function compareRoles(role1, role2) { + assert.strictEqual(typeof role1, 'string'); + assert.strictEqual(typeof role2, 'string'); + + const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ]; + + const roleInt1 = ORDERED_ROLES.indexOf(role1); + const roleInt2 = ORDERED_ROLES.indexOf(role2); + + return roleInt1 - roleInt2; +} + async function add(email, data, auditSource) { assert.strictEqual(typeof email, 'string'); assert(data && typeof data === 'object'); @@ -290,181 +310,6 @@ async function add(email, data, auditSource) { return user.id; } -async function setGhost(user, password, expiresAt) { - assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof expiresAt, 'number'); - - if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet'); - - expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME); - - const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {}; - ghostData[user.username] = { password, expiresAt }; - - await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData); -} - -// returns true if ghost user was matched -async function verifyGhost(username, password) { - assert.strictEqual(typeof username, 'string'); - assert.strictEqual(typeof password, 'string'); - - const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {}; - - // either the username is an object with { password, expiresAt } or a string with the password which will expire on first match - if (username in ghostData) { - if (typeof ghostData[username] === 'object') { - if (ghostData[username].expiresAt < Date.now()) { - debug('verifyGhost: password expired'); - delete ghostData[username]; - - await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData); - - return false; - } else if (ghostData[username].password === password) { - debug('verifyGhost: matched ghost user'); - return true; - } else { - return false; - } - } else if(ghostData[username] === password) { - debug('verifyGhost: matched ghost user'); - delete ghostData[username]; - - await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData); - return true; - } - } - - return false; -} - -async function verifyAppPassword(userId, password, identifier) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof identifier, 'string'); - - const results = await appPasswords.list(userId); - - const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword); - const hash = crypto.createHash('sha256').update(password).digest('base64'); - - if (hashedPasswords.includes(hash)) return; - - throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Password is not valid'); -} - -// identifier is only used to check if password is valid for a specific app -async function verify(user, password, identifier, options) { - assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof options, 'object'); - - if (!user.active) { - debug(`verify: ${user.username} is not active`); - throw new BoxError(BoxError.NOT_FOUND, 'User not active'); - } - - // for just invited users the username may be still null - if (user.username) { - const valid = await verifyGhost(user.username, password); - - if (valid) { - debug(`verify: ${user.username} authenticated via impersonation`); - user.ghost = true; - return user; - } - } - - const [error] = await safe(verifyAppPassword(user.id, password, identifier)); - if (!error) { // matched app password - debug(`verify: ${user.username || user.id} matched app password`); - user.appPassword = true; - return user; - } - - let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds - if (user.source === 'ldap') { - await externalLdap.verifyPassword(user.username, password, options); - const externalLdapConfig = await externalLdap.getConfig(); - localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig); - } else { - const saltBinary = Buffer.from(user.salt, 'hex'); - const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST)); - if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error); - - const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex'); - if (derivedKeyHex !== user.password) { - debug(`verify: ${user.username || user.id} provided incorrect password`); - throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password'); - } - - localTotpCheck = user.twoFactorAuthenticationEnabled; - } - - if (localTotpCheck && !options.skipTotpCheck) { - if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided'); - const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 }); - if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken'); - } - - return user; -} - -async function verifyWithId(id, password, identifier, options) { - assert.strictEqual(typeof id, 'string'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof options, 'object'); - - const user = await get(id); - if (!user) { - debug(`verifyWithId: ${id} not found`); - throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - } - - return await verify(user, password, identifier, options); -} - -async function verifyWithUsername(username, password, identifier, options) { - assert.strictEqual(typeof username, 'string'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof options, 'object'); - - const user = await getByUsername(username.toLowerCase()); - if (user) return await verify(user, password, identifier, options); - - const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase())); - if (error && error.reason === BoxError.BAD_STATE) { - debug(`verifyWithUsername: ${username} not found`); - throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create - } - if (error) { - debug(`verifyWithUsername: failed to auto create user ${username}. %o`, error); - throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - } - - return await verifyWithId(newUserId, password, identifier, options); -} - -async function verifyWithEmail(email, password, identifier, options) { - assert.strictEqual(typeof email, 'string'); - assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof options, 'object'); - - const user = await getByEmail(email.toLowerCase()); - if (!user) { - debug(`verifyWithEmail: ${email} no such user`); - throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - } - - return await verify(user, password, identifier, options); -} - async function del(user, auditSource) { assert.strictEqual(typeof user, 'object'); assert(auditSource && typeof auditSource === 'object'); @@ -712,6 +557,181 @@ async function getSuperadmins() { return await listByRole(exports.ROLE_OWNER); } +async function setGhost(user, password, expiresAt) { + assert.strictEqual(typeof user, 'object'); + assert.strictEqual(typeof password, 'string'); + assert.strictEqual(typeof expiresAt, 'number'); + + if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet'); + + expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME); + + const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {}; + ghostData[user.username] = { password, expiresAt }; + + await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData); +} + +// returns true if ghost user was matched +async function verifyGhost(username, password) { + assert.strictEqual(typeof username, 'string'); + assert.strictEqual(typeof password, 'string'); + + const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {}; + + // either the username is an object with { password, expiresAt } or a string with the password which will expire on first match + if (username in ghostData) { + if (typeof ghostData[username] === 'object') { + if (ghostData[username].expiresAt < Date.now()) { + debug('verifyGhost: password expired'); + delete ghostData[username]; + + await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData); + + return false; + } else if (ghostData[username].password === password) { + debug('verifyGhost: matched ghost user'); + return true; + } else { + return false; + } + } else if(ghostData[username] === password) { + debug('verifyGhost: matched ghost user'); + delete ghostData[username]; + + await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData); + return true; + } + } + + return false; +} + +async function verifyAppPassword(userId, password, identifier) { + assert.strictEqual(typeof userId, 'string'); + assert.strictEqual(typeof password, 'string'); + assert.strictEqual(typeof identifier, 'string'); + + const results = await appPasswords.list(userId); + + const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword); + const hash = crypto.createHash('sha256').update(password).digest('base64'); + + if (hashedPasswords.includes(hash)) return; + + throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Password is not valid'); +} + +// identifier is only used to check if password is valid for a specific app +async function verify(user, password, identifier, options) { + assert.strictEqual(typeof user, 'object'); + assert.strictEqual(typeof password, 'string'); + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof options, 'object'); + + if (!user.active) { + debug(`verify: ${user.username} is not active`); + throw new BoxError(BoxError.NOT_FOUND, 'User not active'); + } + + // for just invited users the username may be still null + if (user.username) { + const valid = await verifyGhost(user.username, password); + + if (valid) { + debug(`verify: ${user.username} authenticated via impersonation`); + user.ghost = true; + return user; + } + } + + const [error] = await safe(verifyAppPassword(user.id, password, identifier)); + if (!error) { // matched app password + debug(`verify: ${user.username || user.id} matched app password`); + user.appPassword = true; + return user; + } + + let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds + if (user.source === 'ldap') { + await externalLdap.verifyPassword(user.username, password, options); + const externalLdapConfig = await externalLdap.getConfig(); + localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig); + } else { + const saltBinary = Buffer.from(user.salt, 'hex'); + const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST)); + if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error); + + const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex'); + if (derivedKeyHex !== user.password) { + debug(`verify: ${user.username || user.id} provided incorrect password`); + throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password'); + } + + localTotpCheck = user.twoFactorAuthenticationEnabled; + } + + if (localTotpCheck && !options.skipTotpCheck) { + if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided'); + const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 }); + if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken'); + } + + return user; +} + +async function verifyWithId(id, password, identifier, options) { + assert.strictEqual(typeof id, 'string'); + assert.strictEqual(typeof password, 'string'); + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof options, 'object'); + + const user = await get(id); + if (!user) { + debug(`verifyWithId: ${id} not found`); + throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + } + + return await verify(user, password, identifier, options); +} + +async function verifyWithUsername(username, password, identifier, options) { + assert.strictEqual(typeof username, 'string'); + assert.strictEqual(typeof password, 'string'); + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof options, 'object'); + + const user = await getByUsername(username.toLowerCase()); + if (user) return await verify(user, password, identifier, options); + + const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase())); + if (error && error.reason === BoxError.BAD_STATE) { + debug(`verifyWithUsername: ${username} not found`); + throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create + } + if (error) { + debug(`verifyWithUsername: failed to auto create user ${username}. %o`, error); + throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + } + + return await verifyWithId(newUserId, password, identifier, options); +} + +async function verifyWithEmail(email, password, identifier, options) { + assert.strictEqual(typeof email, 'string'); + assert.strictEqual(typeof password, 'string'); + assert.strictEqual(typeof identifier, 'string'); + assert.strictEqual(typeof options, 'object'); + + const user = await getByEmail(email.toLowerCase()); + if (!user) { + debug(`verifyWithEmail: ${email} no such user`); + throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + } + + return await verify(user, password, identifier, options); +} + async function getPasswordResetLink(user, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof auditSource, 'object'); @@ -969,24 +989,6 @@ async function disableTwoFactorAuthentication(user, auditSource) { await update(user, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, auditSource); } -function validateRole(role) { - assert.strictEqual(typeof role, 'string'); - - if (ORDERED_ROLES.indexOf(role) !== -1) return null; - - return new BoxError(BoxError.BAD_FIELD, `Invalid role '${role}'`); -} - -function compareRoles(role1, role2) { - assert.strictEqual(typeof role1, 'string'); - assert.strictEqual(typeof role2, 'string'); - - const roleInt1 = ORDERED_ROLES.indexOf(role1); - const roleInt2 = ORDERED_ROLES.indexOf(role2); - - return roleInt1 - roleInt2; -} - async function getAvatar(user) { assert.strictEqual(typeof user, 'object'); @@ -1042,3 +1044,4 @@ function parseDisplayName(displayName) { const lastName = displayName.substring(idx+1); return { firstName, lastName, middleName }; } + diff --git a/src/volumes.js b/src/volumes.js index f580432f4..66436ef96 100644 --- a/src/volumes.js +++ b/src/volumes.js @@ -1,5 +1,21 @@ 'use strict'; +exports = module.exports = { + add, + get, + update, + del, + list, + remount, + getStatus, + removePrivateFields, + + mountAll, + + // exported for testing + _validateHostPath: validateHostPath +}; + const assert = require('node:assert'), BoxError = require('./boxerror.js'), crypto = require('node:crypto'), @@ -202,18 +218,3 @@ async function mountAll() { } } -exports = module.exports = { - add, - get, - update, - del, - list, - remount, - getStatus, - removePrivateFields, - - mountAll, - - // exported for testing - _validateHostPath: validateHostPath -};