From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id C2C366AC74 for ; Mon, 15 Mar 2021 18:22:18 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B25432537D for ; Mon, 15 Mar 2021 18:22:18 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id DB18C2536D for ; Mon, 15 Mar 2021 18:22:16 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id A70EF42660 for ; Mon, 15 Mar 2021 18:22:16 +0100 (CET) Date: Mon, 15 Mar 2021 18:22:14 +0100 From: Stoiko Ivanov To: Wolfgang Bumiller Cc: pmg-devel@lists.proxmox.com Message-ID: <20210315182214.25eac96b@rosa.proxmox.com> In-Reply-To: <20210312152421.30114-23-w.bumiller@proxmox.com> References: <20210312152421.30114-1-w.bumiller@proxmox.com> <20210312152421.30114-23-w.bumiller@proxmox.com> X-Mailer: Claws Mail 3.17.3 (GTK+ 2.24.32; x86_64-pc-linux-gnu) MIME-Version: 1.0 Content-Type: text/plain; charset=US-ASCII Content-Transfer-Encoding: 7bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.066 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: Re: [pmg-devel] [PATCH v2 widget-toolkit 4/7] add certificate panel X-BeenThere: pmg-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Mail Gateway development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 15 Mar 2021 17:22:18 -0000 On Fri, 12 Mar 2021 16:24:11 +0100 Wolfgang Bumiller wrote: > Again, initially copied from PVE but adapted so it can be > used by both. (PVE side still needs to be tested though.) > > The 'nodename' property is optional (since on PMG we > currently don't expose them via the UI directly). Instead, > the certificate info URL is required and the 'uploadButtons' > need to be passed, which just contains the certificate > "name", id (filename), url, and whether it is deletable and > whether a GUI reload is required after changing it. If only > 1 entry is passed, the button stays a regular button (that > way PVE should still look the same), whereas in PMG we have > a menu to select between API and SMTP certificates. > > Signed-off-by: Wolfgang Bumiller > --- > * No changes since v1 > > src/Makefile | 2 + > src/panel/Certificates.js | 267 +++++++++++++++++++++++++++++++++++++ > src/window/Certificates.js | 205 ++++++++++++++++++++++++++++ > 3 files changed, 474 insertions(+) > create mode 100644 src/panel/Certificates.js > create mode 100644 src/window/Certificates.js > > diff --git a/src/Makefile b/src/Makefile > index d0435b8..d782e92 100644 > --- a/src/Makefile > +++ b/src/Makefile > @@ -49,6 +49,7 @@ JSSRC= \ > panel/PruneKeepPanel.js \ > panel/RRDChart.js \ > panel/GaugeWidget.js \ > + panel/Certificates.js \ > window/Edit.js \ > window/PasswordEdit.js \ > window/SafeDestroy.js \ > @@ -56,6 +57,7 @@ JSSRC= \ > window/LanguageEdit.js \ > window/DiskSmart.js \ > window/ZFSDetail.js \ > + window/Certificates.js \ > node/APT.js \ > node/NetworkEdit.js \ > node/NetworkView.js \ > diff --git a/src/panel/Certificates.js b/src/panel/Certificates.js > new file mode 100644 > index 0000000..332a189 > --- /dev/null > +++ b/src/panel/Certificates.js > @@ -0,0 +1,267 @@ > +Ext.define('Proxmox.panel.Certificates', { > + extend: 'Ext.grid.Panel', > + xtype: 'pmxCertificates', > + > + // array of { name, id (=filename), url, deletable, reloadUi } > + uploadButtons: undefined, > + > + // The /info path for the current node. > + infoUrl: undefined, > + > + columns: [ > + { > + header: gettext('File'), > + width: 150, > + dataIndex: 'filename', > + }, > + { > + header: gettext('Issuer'), > + flex: 1, > + dataIndex: 'issuer', > + }, > + { > + header: gettext('Subject'), > + flex: 1, > + dataIndex: 'subject', > + }, > + { > + header: gettext('Public Key Alogrithm'), > + flex: 1, > + dataIndex: 'public-key-type', > + hidden: true, > + }, > + { > + header: gettext('Public Key Size'), > + flex: 1, > + dataIndex: 'public-key-bits', > + hidden: true, > + }, > + { > + header: gettext('Valid Since'), > + width: 150, > + dataIndex: 'notbefore', > + renderer: Proxmox.Utils.render_timestamp, > + }, > + { > + header: gettext('Expires'), > + width: 150, > + dataIndex: 'notafter', > + renderer: Proxmox.Utils.render_timestamp, > + }, > + { > + header: gettext('Subject Alternative Names'), > + flex: 1, > + dataIndex: 'san', > + renderer: Proxmox.Utils.render_san, > + }, > + { > + header: gettext('Fingerprint'), > + dataIndex: 'fingerprint', > + hidden: true, > + }, > + { > + header: gettext('PEM'), > + dataIndex: 'pem', > + hidden: true, > + }, > + ], > + > + reload: function() { > + let me = this; > + me.rstore.load(); > + }, > + > + delete_certificate: function() { > + let me = this; > + > + let rec = me.selModel.getSelection()[0]; > + if (!rec) { > + return; > + } > + > + let cert = me.certById[rec.id]; > + let url = cert.url; > + Proxmox.Utils.API2Request({ > + url: `/api2/extjs/${url}?restart=1`, > + method: 'DELETE', > + success: function(response, opt) { > + if (cert.reloadUid) { > + let txt = > + gettext('GUI will be restarted with new certificates, please reload!'); > + Ext.getBody().mask(txt, ['x-mask-loading']); > + // reload after 10 seconds automatically > + Ext.defer(function() { > + window.location.reload(true); > + }, 10000); > + } > + }, > + failure: function(response, opt) { > + Ext.Msg.alert(gettext('Error'), response.htmlStatus); > + }, > + }); > + }, > + > + controller: { > + xclass: 'Ext.app.ViewController', > + view_certificate: function() { > + let me = this; > + let view = me.getView(); > + > + let selection = view.getSelection(); > + if (!selection || selection.length < 1) { > + return; > + } > + let win = Ext.create('Proxmox.window.CertificateViewer', { > + cert: selection[0].data.filename, > + url: `/api2/extjs/${view.infoUrl}`, > + }); > + win.show(); > + }, > + }, > + > + listeners: { > + itemdblclick: 'view_certificate', > + }, > + > + initComponent: function() { > + let me = this; > + > + if (!me.nodename) { > + // only used for the store name > + me.nodename = "_all"; > + } > + > + if (!me.uploadButtons) { > + throw "no upload buttons defined"; > + } > + > + if (!me.infoUrl) { > + throw "no certificate store url given"; > + } > + > + me.rstore = Ext.create('Proxmox.data.UpdateStore', { > + storeid: 'certs-' + me.nodename, > + model: 'proxmox-certificate', > + proxy: { > + type: 'proxmox', > + url: `/api2/extjs/${me.infoUrl}`, > + }, > + }); > + > + me.store = { > + type: 'diff', > + rstore: me.rstore, > + }; > + > + let tbar = []; > + > + me.deletableCertIds = {}; > + me.certById = {}; > + if (me.uploadButtons.length === 1) { > + let cert = me.uploadButtons[0]; > + > + if (!cert.url) { > + throw "missing certificate url"; > + } > + > + me.certById[cert.id] = cert; > + > + if (cert.deletable) { > + me.deletableCertIds[cert.id] = true; > + } > + > + tbar.push( > + { > + xtype: 'button', > + text: gettext('Upload Custom Certificate'), > + handler: function() { > + let grid = this.up('grid'); > + let win = Ext.create('Proxmox.window.CertificateUpload', { > + url: `/api2/extjs/${cert.url}`, > + reloadUi: cert.reloadUi, > + }); > + win.show(); > + win.on('destroy', grid.reload, grid); > + }, > + }, > + ); > + } else { > + let items = []; > + > + me.selModel = Ext.create('Ext.selection.RowModel', {}); > + > + for (const cert of me.uploadButtons) { > + if (!cert.id) { > + throw "missing id in certificate entry"; > + } > + > + if (!cert.url) { > + throw "missing url in certificate entry"; > + } > + > + if (!cert.name) { > + throw "missing name in certificate entry"; > + } > + > + me.certById[cert.id] = cert; > + > + if (cert.deletable) { > + me.deletableCertIds[cert.id] = true; > + } > + > + items.push({ > + text: Ext.String.format('Upload {0} Certificate', cert.name), > + handler: function() { > + let grid = this.up('grid'); > + let win = Ext.create('Proxmox.window.CertificateUpload', { > + url: `/api2/extjs/${cert.url}`, > + reloadUi: cert.reloadUi, > + }); > + win.show(); > + win.on('destroy', grid.reload, grid); > + }, > + }); > + } > + > + tbar.push( > + { > + text: gettext('Upload Custom Certificate'), > + menu: { > + xtype: 'menu', > + items, > + }, > + }, > + ); > + } > + > + tbar.push( > + { > + xtype: 'proxmoxButton', > + text: gettext('Delete Custom Certificate'), > + confirmMsg: rec => Ext.String.format( > + gettext('Are you sure you want to remove the certificate used for {0}'), > + me.certById[rec.id].name, > + ), > + callback: () => me.reload(), > + selModel: me.selModel, > + disabled: true, > + enableFn: rec => !!me.deletableCertIds[rec.id], > + handler: function() { me.delete_certificate(); }, > + }, > + '-', > + { > + xtype: 'proxmoxButton', > + itemId: 'viewbtn', > + disabled: true, > + text: gettext('View Certificate'), > + handler: 'view_certificate', > + }, > + ); > + Ext.apply(me, { tbar }); > + > + me.callParent(); > + > + me.rstore.startUpdate(); > + me.on('destroy', me.rstore.stopUpdate, me.rstore); > + }, > +}); > diff --git a/src/window/Certificates.js b/src/window/Certificates.js > new file mode 100644 > index 0000000..1bdf394 > --- /dev/null > +++ b/src/window/Certificates.js > @@ -0,0 +1,205 @@ > +Ext.define('Proxmox.window.CertificateViewer', { > + extend: 'Proxmox.window.Edit', > + xtype: 'pmxCertViewer', > + > + title: gettext('Certificate'), > + > + fieldDefaults: { > + labelWidth: 120, > + }, > + width: 800, > + resizable: true, > + > + items: [ > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Name'), > + name: 'filename', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Fingerprint'), > + name: 'fingerprint', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Issuer'), > + name: 'issuer', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Subject'), > + name: 'subject', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Public Key Type'), > + name: 'public-key-type', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Public Key Size'), > + name: 'public-key-bits', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Valid Since'), > + renderer: Proxmox.Utils.render_timestamp, > + name: 'notbefore', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Expires'), > + renderer: Proxmox.Utils.render_timestamp, > + name: 'notafter', > + }, > + { > + xtype: 'displayfield', > + fieldLabel: gettext('Subject Alternative Names'), > + name: 'san', > + renderer: Proxmox.Utils.render_san, > + }, > + { > + xtype: 'textarea', > + editable: false, > + grow: true, > + growMax: 200, > + fieldLabel: gettext('Certificate'), > + name: 'pem', > + }, > + ], > + > + initComponent: function() { > + var me = this; > + > + if (!me.cert) { > + throw "no cert given"; > + } > + > + if (!me.url) { > + throw "no url given"; > + } > + > + me.callParent(); > + > + // hide OK/Reset button, because we just want to show data > + me.down('toolbar[dock=bottom]').setVisible(false); > + > + me.load({ > + success: function(response) { > + if (Ext.isArray(response.result.data)) { > + Ext.Array.each(response.result.data, function(item) { > + if (item.filename === me.cert) { > + me.setValues(item); > + return false; > + } > + return true; > + }); > + } > + }, > + }); > + }, > +}); > + > +Ext.define('Proxmox.window.CertificateUpload', { > + extend: 'Proxmox.window.Edit', > + xtype: 'pmxCertUpload', > + > + title: gettext('Upload Custom Certificate'), > + resizable: false, > + isCreate: true, > + submitText: gettext('Upload'), > + method: 'POST', > + width: 600, > + > + // whether the UI needs a reload after this > + reloadUi: undefined, > + > + apiCallDone: function(success, response, options) { > + let me = this; > + > + if (!success || !me.reloadUi) { > + return; > + } > + > + var txt = gettext('GUI server will be restarted with new certificates, please reload!'); > + Ext.getBody().mask(txt, ['pve-static-mask']); > + // reload after 10 seconds automatically > + Ext.defer(function() { > + window.location.reload(true); > + }, 10000); > + }, > + > + items: [ > + { > + fieldLabel: gettext('Private Key (Optional)'), > + labelAlign: 'top', > + emptyText: gettext('No change'), > + name: 'key', > + xtype: 'textarea', > + }, > + { > + xtype: 'filebutton', > + text: gettext('From File'), > + listeners: { > + change: function(btn, e, value) { > + let form = this.up('form'); > + e = e.event; > + Ext.Array.each(e.target.files, function(file) { > + Proxmox.Utils.loadSSHKeyFromFile(file, function(res) { small glitch - Proxmox.Utils.loadSSHKeyFromFile does not exist - probably Proxmox.Utils.loadTextFromFile was meant > + form.down('field[name=key]').setValue(res); > + }); > + }); > + btn.reset(); > + }, > + }, > + }, > + { > + xtype: 'box', > + autoEl: 'hr', > + }, > + { > + fieldLabel: gettext('Certificate Chain'), > + labelAlign: 'top', > + allowBlank: false, > + name: 'certificates', > + xtype: 'textarea', > + }, > + { > + xtype: 'filebutton', > + text: gettext('From File'), > + listeners: { > + change: function(btn, e, value) { > + let form = this.up('form'); > + e = e.event; > + Ext.Array.each(e.target.files, function(file) { > + Proxmox.Utils.loadSSHKeyFromFile(file, function(res) { same here > + form.down('field[name=certificates]').setValue(res); > + }); > + }); > + btn.reset(); > + }, > + }, > + }, > + { > + xtype: 'hidden', > + name: 'restart', > + value: '1', > + }, > + { > + xtype: 'hidden', > + name: 'force', > + value: '1', > + }, > + ], > + > + initComponent: function() { > + var me = this; > + > + if (!me.url) { > + throw "neither url given"; > + } > + > + me.callParent(); > + }, > +});