[pmg-devel] [PATCH v2 widget-toolkit 4/7] add certificate panel
Stoiko Ivanov
s.ivanov at proxmox.com
Mon Mar 15 18:22:14 CET 2021
On Fri, 12 Mar 2021 16:24:11 +0100
Wolfgang Bumiller <w.bumiller at proxmox.com> 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 <w.bumiller at proxmox.com>
> ---
> * 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();
> + },
> +});
More information about the pmg-devel
mailing list