diff --git a/CHANGES b/CHANGES index e7f76f929..f7c731d75 100644 --- a/CHANGES +++ b/CHANGES @@ -2402,4 +2402,5 @@ [7.1.0] * Add mail manager role * mailbox: app can be set as owner when recvmail addon enabled +* domains: add well known config UI (for jitsi configuration) diff --git a/src/domains.js b/src/domains.js index 064f49e3f..5cf0ca39a 100644 --- a/src/domains.js +++ b/src/domains.js @@ -4,7 +4,8 @@ module.exports = exports = { add, get, list, - update, + setConfig, + setWellKnown, del, clear, @@ -189,7 +190,7 @@ async function list() { return results; } -async function update(domain, data, auditSource) { +async function setConfig(domain, data, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof data.zoneName, 'string'); assert.strictEqual(typeof data.provider, 'string'); @@ -198,7 +199,7 @@ async function update(domain, data, auditSource) { assert.strictEqual(typeof data.tlsConfig, 'object'); assert.strictEqual(typeof auditSource, 'object'); - let { zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown } = data; + let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data; let error; if (settings.isDemo() && (domain === settings.dashboardDomain())) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'); @@ -218,9 +219,6 @@ async function update(domain, data, auditSource) { error = validateTlsConfig(tlsConfig, provider); if (error) throw error; - error = validateWellKnown(wellKnown, provider); - if (error) throw error; - if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config); const result = await verifyDnsConfig(config, domain, zoneName, provider); @@ -231,14 +229,13 @@ async function update(domain, data, auditSource) { zoneName, provider, tlsConfig, - wellKnown, }; if (fallbackCertificate) newData.fallbackCertificate = fallbackCertificate; let args = [ ], fields = [ ]; for (const k in newData) { - if (k === 'config' || k === 'tlsConfig' || k === 'wellKnown' || k === 'fallbackCertificate') { // json fields + if (k === 'config' || k === 'tlsConfig' || k === 'fallbackCertificate') { // json fields fields.push(`${k}Json = ?`); args.push(JSON.stringify(newData[k])); } else { @@ -259,6 +256,21 @@ async function update(domain, data, auditSource) { eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider }); } +async function setWellKnown(domain, wellKnown, auditSource) { + assert.strictEqual(typeof domain, 'string'); + assert.strictEqual(typeof wellKnown, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + + let error = validateWellKnown(wellKnown); + if (error) throw error; + + [error] = await safe(database.query('UPDATE domains SET wellKnownJson = ? WHERE domain=?', [ JSON.stringify(wellKnown), domain ])); + if (error && error.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found'); + if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); + + eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, wellKnown }); +} + async function del(domain, auditSource) { assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof auditSource, 'object'); diff --git a/src/routes/domains.js b/src/routes/domains.js index fb76b39eb..978d696ee 100644 --- a/src/routes/domains.js +++ b/src/routes/domains.js @@ -4,7 +4,8 @@ exports = module.exports = { add, get, list, - update, + setConfig, + setWellKnown, del, checkDnsRecords, @@ -76,7 +77,7 @@ async function list(req, res, next) { next(new HttpSuccess(200, { domains: results })); } -async function update(req, res, next) { +async function setConfig(req, res, next) { assert.strictEqual(typeof req.params.domain, 'string'); assert.strictEqual(typeof req.body, 'object'); @@ -98,26 +99,33 @@ async function update(req, res, next) { if (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string') return next(new HttpError(400, 'tlsConfig.provider must be a string')); } - if ('wellKnown' in req.body) { - if (typeof req.body.wellKnown !== 'object') return next(new HttpError(400, 'wellKnown must be an object')); - if (req.body.wellKnown) { - if (Object.keys(req.body.wellKnown).some(k => typeof req.body.wellKnown[k] !== 'string')) return next(new HttpError(400, 'wellKnown is a map of strings')); - } - } - // some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26) req.clearTimeout(); - let data = { + const data = { zoneName: req.body.zoneName || '', provider: req.body.provider, config: req.body.config, fallbackCertificate: req.body.fallbackCertificate || null, - tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' }, - wellKnown: req.body.wellKnown || null + tlsConfig: req.body.tlsConfig || { provider: 'letsencrypt-prod' } }; - const [error] = await safe(domains.update(req.params.domain, data, AuditSource.fromRequest(req))); + const [error] = await safe(domains.setConfig(req.params.domain, data, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(204, {})); +} + +async function setWellKnown(req, res, next) { + assert.strictEqual(typeof req.params.domain, 'string'); + assert.strictEqual(typeof req.body, 'object'); + + if (typeof req.body.wellKnown !== 'object') return next(new HttpError(400, 'wellKnown must be an object')); + if (req.body.wellKnown) { + if (Object.keys(req.body.wellKnown).some(k => typeof req.body.wellKnown[k] !== 'string')) return next(new HttpError(400, 'wellKnown is a map of strings')); + } + + const [error] = await safe(domains.setWellKnown(req.params.domain, req.body.wellKnown || null, AuditSource.fromRequest(req))); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(204, {})); diff --git a/src/routes/test/domains-test.js b/src/routes/test/domains-test.js index 8c235199c..8e1af5d3c 100644 --- a/src/routes/test/domains-test.js +++ b/src/routes/test/domains-test.js @@ -152,6 +152,40 @@ describe('Domains API', function () { }); }); + describe('update', function () { + it('config fails for non-existing domain', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/domains/whatever/update`) + .query({ access_token: owner.token }) + .ok(() => true); + + expect(response.statusCode).to.equal(404); + }); + + it('config succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/config`) + .query({ access_token: owner.token }) + .send(DOMAIN_0); + + expect(response.statusCode).to.equal(204); + }); + + it('wellknown succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/wellknown`) + .query({ access_token: owner.token }) + .send({ wellKnown: null }); + + expect(response.statusCode).to.equal(204); + }); + + it('wellknown succeeds', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/wellknown`) + .query({ access_token: owner.token }) + .send({ wellKnown: { service: 'some.service' } }); + + expect(response.statusCode).to.equal(204); + }); + }); + describe('Certificates API', function () { let validCert0, validKey0, // example.com validCert1, validKey1; // *.example.com @@ -170,7 +204,7 @@ describe('Domains API', function () { let d = Object.assign({}, DOMAIN_0); d.fallbackCertificate = { key: validKey1 }; - const response = await superagent.put(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}`) + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/config`) .query({ access_token: owner.token }) .send(d) .ok(() => true); @@ -182,7 +216,7 @@ describe('Domains API', function () { let d = Object.assign({}, DOMAIN_0); d.fallbackCertificate = { cert: validCert1 }; - const response = await superagent.put(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}`) + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/config`) .query({ access_token: owner.token }) .send(d) .ok(() => true); @@ -194,7 +228,7 @@ describe('Domains API', function () { let d = Object.assign({}, DOMAIN_0); d.fallbackCertificate = { cert: 1234, key: validKey1 }; - const response = await superagent.put(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}`) + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/config`) .query({ access_token: owner.token }) .send(d) .ok(() => true); @@ -206,7 +240,7 @@ describe('Domains API', function () { let d = Object.assign({}, DOMAIN_0); d.fallbackCertificate = { cert: validCert1, key: true }; - const response = await superagent.put(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}`) + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/config`) .query({ access_token: owner.token }) .send(d) .ok(() => true); @@ -218,7 +252,7 @@ describe('Domains API', function () { let d = Object.assign({}, DOMAIN_0); d.fallbackCertificate = { cert: validCert0, key: validKey0 }; - const response = await superagent.put(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}`) + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/config`) .query({ access_token: owner.token }) .send(d) .ok(() => true); @@ -230,7 +264,7 @@ describe('Domains API', function () { let d = Object.assign({}, DOMAIN_0); d.fallbackCertificate = { cert: validCert1, key: validKey1 }; - const response = await superagent.put(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}`) + const response = await superagent.post(`${serverUrl}/api/v1/domains/${DOMAIN_0.domain}/config`) .query({ access_token: owner.token }) .send(d); diff --git a/src/server.js b/src/server.js index f59a73837..2bde61879 100644 --- a/src/server.js +++ b/src/server.js @@ -313,7 +313,8 @@ function initializeExpressSync() { router.post('/api/v1/domains', json, token, authorizeAdmin, routes.domains.add); router.get ('/api/v1/domains', token, routes.domains.list); router.get ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.get); // this is manage scope because it returns non-restricted fields - router.put ('/api/v1/domains/:domain', json, token, authorizeAdmin, routes.domains.update); + router.post('/api/v1/domains/:domain/config', json, token, authorizeAdmin, routes.domains.setConfig); + router.post('/api/v1/domains/:domain/wellknown', json, token, authorizeAdmin, routes.domains.setWellKnown); router.del ('/api/v1/domains/:domain', token, authorizeAdmin, routes.domains.del); router.get ('/api/v1/domains/:domain/dns_check', token, authorizeAdmin, routes.domains.checkDnsRecords); diff --git a/src/test/dns-providers-test.js b/src/test/dns-providers-test.js index 7494d8d4a..e1801a739 100644 --- a/src/test/dns-providers-test.js +++ b/src/test/dns-providers-test.js @@ -28,7 +28,7 @@ describe('dns provider', function () { domainCopy.provider = 'noop'; domainCopy.config = {}; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('upsert succeeds', async function () { @@ -55,7 +55,7 @@ describe('dns provider', function () { token: TOKEN }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('upsert non-existing record succeeds', async function () { @@ -292,7 +292,7 @@ describe('dns provider', function () { apiSecret: SECRET }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('upsert record succeeds', async function () { @@ -367,7 +367,7 @@ describe('dns provider', function () { token: TOKEN }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('upsert record succeeds', async function () { @@ -430,7 +430,7 @@ describe('dns provider', function () { token: TOKEN }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('upsert record succeeds', async function () { @@ -530,7 +530,7 @@ describe('dns provider', function () { token: token }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); beforeEach(function () { @@ -942,7 +942,7 @@ describe('dns provider', function () { AWS._originalRoute53 = AWS.Route53; AWS.Route53 = Route53Mock; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); after(function () { @@ -1078,7 +1078,7 @@ describe('dns provider', function () { _OriginalGCDNS = GCDNS.prototype.getZones; GCDNS.prototype.getZones = mockery(zoneQueue); - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); after(function () { diff --git a/src/test/domains-test.js b/src/test/domains-test.js index cfab4fd93..51205b2e6 100644 --- a/src/test/domains-test.js +++ b/src/test/domains-test.js @@ -52,12 +52,12 @@ describe('Domains', function () { expect(result).to.be(null); }); - it('can update domain', async function () { + it('can set domain config', async function () { const newConfig = {}; const newTlsConfig = { provider: 'letsencrypt-staging' }; const newDomain = Object.assign({}, DOMAIN_0, { config: newConfig, tlsConfig: newTlsConfig }); - await domains.update(DOMAIN_0.domain, newDomain, auditSource); + await domains.setConfig(DOMAIN_0.domain, newDomain, auditSource); const result = await domains.get(DOMAIN_0.domain); expect(result.domain).to.equal(DOMAIN_0.domain); @@ -70,6 +70,16 @@ describe('Domains', function () { DOMAIN_0.tlsConfig = newTlsConfig; }); + it('can set domain wellknown', async function () { + await domains.setWellKnown(DOMAIN_0.domain, { service: 'some.service' }, auditSource); + let result = await domains.get(DOMAIN_0.domain); + expect(result.wellKnown).to.eql({ service: 'some.service' }); + + await domains.setWellKnown(DOMAIN_0.domain, null, auditSource); + result = await domains.get(DOMAIN_0.domain); + expect(result.wellKnown).to.eql(null); + }); + it('can get all domains', async function () { const result = await domains.list(); expect(result.length).to.equal(2); diff --git a/src/test/reverseproxy-test.js b/src/test/reverseproxy-test.js index eb0a12a05..828bab2af 100644 --- a/src/test/reverseproxy-test.js +++ b/src/test/reverseproxy-test.js @@ -143,7 +143,7 @@ describe('Reverse Proxy', function () { before(async function () { domainCopy.tlsConfig = { provider: 'letsencrypt-prod' }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('returns prod acme in prod cloudron', async function () { @@ -157,7 +157,7 @@ describe('Reverse Proxy', function () { before(async function () { domainCopy.tlsConfig = { provider: 'letsencrypt-staging' }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('returns staging acme in prod cloudron', async function () { @@ -171,7 +171,7 @@ describe('Reverse Proxy', function () { before(async function () { domainCopy.tlsConfig = { provider: 'fallback' }; - await domains.update(domainCopy.domain, domainCopy, auditSource); + await domains.setConfig(domainCopy.domain, domainCopy, auditSource); }); it('configure nginx correctly', async function () {