From fb766d14e9e48e0a7d4fd297a08e2911d4ae4bad Mon Sep 17 00:00:00 2001 From: Will Rouesnel Date: Sat, 27 May 2023 02:21:10 +1000 Subject: [PATCH] Add support for writing client CAs when access-lists are updated This commit adds the basic support necessary to produce the combined client CA files when certificates are updated. --- backend/internal/access-list.js | 82 +++++++++++++++++-- .../s6-overlay/s6-rc.d/prepare/20-paths.sh | 1 + frontend/js/app/nginx/access/main.js | 4 +- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/backend/internal/access-list.js b/backend/internal/access-list.js index de71d165..74cbbeef 100644 --- a/backend/internal/access-list.js +++ b/backend/internal/access-list.js @@ -86,7 +86,7 @@ const internalAccessList = { // re-fetch with expansions return internalAccessList.get(access, { id: data.id, - expand: ['owner', 'items', 'clients', 'clientcas', 'proxy_hosts.access_list.[clientcas.certificate,clients,items]'] + expand: ['owner', 'items', 'clients', 'clientcas.certificate', 'proxy_hosts.access_list.[clientcas,clients,items]'] }, true /* <- skip masking */); }) .then((row) => { @@ -247,7 +247,6 @@ const internalAccessList = { }); } }) - .then(internalNginx.reload) .then(() => { // Add to audit log return internalAuditLog.add(access, { @@ -261,10 +260,11 @@ const internalAccessList = { // re-fetch with expansions return internalAccessList.get(access, { id: data.id, - expand: ['owner', 'items', 'clients', 'clientcas', 'proxy_hosts.[certificate,access_list.[clientcas.certificate,clients,items]]'] + expand: ['owner', 'items', 'clients', 'clientcas.certificate', 'proxy_hosts.[certificate,access_list.[clientcas,clients,items]]'] }, true /* <- skip masking */); }) .then((row) => { + console.log(row); return internalAccessList.build(row) .then(() => { if (row.proxy_host_count) { @@ -274,6 +274,11 @@ const internalAccessList = { .then(() => { return internalAccessList.maskItems(row); }); + }) + .then((row) => { + return internalNginx.reload().then(() => { + return row; + }); }); }, @@ -299,7 +304,7 @@ const internalAccessList = { .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') .where('access_list.is_deleted', 0) .andWhere('access_list.id', data.id) - .withGraphFetched('[owner,items,clients,clientcas,proxy_hosts.[certificate,access_list.[clientcas.certificate,clients,items]]]') + .allowGraph('[owner,items,clients,clientcas.certificate,proxy_hosts.[certificate,access_list.[clientcas,clients,items]]]') .first(); if (access_data.permission_visibility !== 'all') { @@ -420,7 +425,7 @@ const internalAccessList = { .joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0') .where('access_list.is_deleted', 0) .groupBy('access_list.id') - .withGraphFetched('[owner,items,clients,clientcas.certificate]') + .allowGraph('[owner,items,clients,clientcas.certificate]') .orderBy('access_list.name', 'ASC'); if (access_data.permission_visibility !== 'all') { @@ -477,6 +482,8 @@ const internalAccessList = { }, /** + * Mask sensitive items in access list responses + * * @param {Object} list * @returns {Object} */ @@ -496,6 +503,24 @@ const internalAccessList = { }); } + // Mask certificates in clientcas responses + if (list && typeof list.clientcas !== 'undefined') { + list.clientcas.map(function(val, idx) { + if (typeof val.certificate !== 'undefined') { + list.clientcas[idx].certificate.meta = {}; + } + }); + } + + // Mask certificates in ProxyHost responses (clear the meta field) + if (list && typeof list.proxy_hosts !== 'undefined') { + list.proxy_hosts.map(function(val, idx) { + if (typeof val.certificate !== 'undefined') { + list.proxy_hosts[idx].certificate.meta = {}; + } + }); + } + return list; }, @@ -508,17 +533,27 @@ const internalAccessList = { return '/data/access/' + list.id; }, + /** + * @param {Object} list + * @param {Integer} list.id + * @returns {String} + */ + getClientCAFilename: (list) => { + return '/data/clientca/' + list.id; + }, + /** * @param {Object} list * @param {Integer} list.id * @param {String} list.name * @param {Array} list.items + * @param {Array} list.clientcas * @returns {Promise} */ build: (list) => { - logger.info('Building Access file #' + list.id + ' for: ' + list.name); - return new Promise((resolve, reject) => { + const htPasswdBuild = new Promise((resolve, reject) => { + logger.info('Building Access file #' + list.id + ' for: ' + list.name); let htpasswd_file = internalAccessList.getFilename(list); // 1. remove any existing access file @@ -566,6 +601,39 @@ const internalAccessList = { }); } }); + + const caCertificateBuild = new Promise((resolve, reject) => { + // TODO: we need to ensure this rebuild is run if any certificates change + logger.info('Building Client CA file #' + list.id + ' for: ' + list.name); + let clientca_file = internalAccessList.getClientCAFilename(list); + + const certificate_bodies = list.clientcas + .filter((clientca) => { + return typeof clientca.certificate.meta !== 'undefined'; + }) + .map((clientca) => { + return clientca.certificate.meta.certificate; + }); + + // Unlink the original file (nginx retains file handle till reload) + try { + fs.unlinkSync(clientca_file); + } catch (err) { + // do nothing + } + + // Write the new file in one shot + try { + fs.writeFileSync(clientca_file, certificate_bodies.join('\n'), {encoding: 'utf8'}); + logger.success('Built Client CA file #' + list.id + ' for: ' + list.name); + resolve(clientca_file); + } catch (err) { + reject(err); + } + }); + + // Execute both promises concurrently + return Promise.all([htPasswdBuild, caCertificateBuild]); } }; diff --git a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh index 2f59ef41..ec0ca734 100755 --- a/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh +++ b/docker/rootfs/etc/s6-overlay/s6-rc.d/prepare/20-paths.sh @@ -20,6 +20,7 @@ mkdir -p \ /data/custom_ssl \ /data/logs \ /data/access \ + /data/clientca \ /data/nginx/default_host \ /data/nginx/default_www \ /data/nginx/proxy_host \ diff --git a/frontend/js/app/nginx/access/main.js b/frontend/js/app/nginx/access/main.js index 94f99de0..79f774c7 100644 --- a/frontend/js/app/nginx/access/main.js +++ b/frontend/js/app/nginx/access/main.js @@ -73,7 +73,7 @@ module.exports = Mn.View.extend({ e.preventDefault(); let query = this.ui.query.val(); - this.fetch(['owner', 'items', 'clients', 'clientcas'], query) + this.fetch(['owner', 'items', 'clients', 'clientcas.certificate'], query) .then(response => this.showData(response)) .catch(err => { this.showError(err); @@ -88,7 +88,7 @@ module.exports = Mn.View.extend({ onRender: function () { let view = this; - view.fetch(['owner', 'items', 'clients', 'clientcas']) + view.fetch(['owner', 'items', 'clients', 'clientcas.certificate']) .then(response => { if (!view.isDestroyed()) { if (response && response.length) {