From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: Wolfgang Bumiller <w.bumiller@proxmox.com>
Cc: pmg-devel@lists.proxmox.com
Subject: Re: [pmg-devel] [PATCH v2 widget-toolkit 4/7] add certificate panel
Date: Mon, 15 Mar 2021 18:22:14 +0100 [thread overview]
Message-ID: <20210315182214.25eac96b@rosa.proxmox.com> (raw)
In-Reply-To: <20210312152421.30114-23-w.bumiller@proxmox.com>
On Fri, 12 Mar 2021 16:24:11 +0100
Wolfgang Bumiller <w.bumiller@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@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();
> + },
> +});
next prev parent reply other threads:[~2021-03-15 17:22 UTC|newest]
Thread overview: 42+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-03-12 15:23 [pmg-devel] [PATCH v2 api/gui/wtk/acme 0/many] Certificates & ACME Wolfgang Bumiller
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 1/8] depend on libpmg-rs-perl and proxmox-acme Wolfgang Bumiller
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 2/8] add PMG::CertHelpers module Wolfgang Bumiller
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 3/8] add PMG::NodeConfig module Wolfgang Bumiller
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 4/8] cluster: sync acme/ and acme-plugins.conf Wolfgang Bumiller
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 5/8] api: add ACME and ACMEPlugin module Wolfgang Bumiller
2021-03-15 11:07 ` Fabian Grünbichler
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 6/8] add certificates api endpoint Wolfgang Bumiller
2021-03-15 11:08 ` Fabian Grünbichler
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 7/8] add node-config api entry points Wolfgang Bumiller
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 8/8] add acme and cert subcommands to pmgconfig Wolfgang Bumiller
2021-03-15 17:57 ` Stoiko Ivanov
2021-03-15 21:39 ` Stoiko Ivanov
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 1/8] depend on libpmg-rs-perl and proxmox-acme Wolfgang Bumiller
2021-03-12 15:23 ` [pmg-devel] [PATCH v2 api 2/8] add PMG::CertHelpers module Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 api 3/8] add PMG::NodeConfig module Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 api 4/8] cluster: sync acme/ and acme-plugins.conf Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 api 5/8] api: add ACME and ACMEPlugin module Wolfgang Bumiller
2021-03-15 18:37 ` Stoiko Ivanov
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 api 6/8] add certificates api endpoint Wolfgang Bumiller
2021-03-15 18:14 ` Stoiko Ivanov
2021-03-15 20:51 ` Stoiko Ivanov
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 api 7/8] add node-config api entry points Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 api 8/8] add acme and cert subcommands to pmgconfig Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 gui] add certificates and acme view Wolfgang Bumiller
2021-03-12 15:24 ` Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 1/7] Utils: add ACME related utilities Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 2/7] add ACME related data models Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 3/7] add ACME forms Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 4/7] add certificate panel Wolfgang Bumiller
2021-03-15 17:22 ` Stoiko Ivanov [this message]
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 5/7] add ACME account panel Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 6/7] add ACME plugin editing Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 7/7] add ACME domain editing Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 1/7] Utils: add ACME related utilities Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 2/7] add ACME related data models Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 3/7] add ACME forms Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 4/7] add certificate panel Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 5/7] add ACME account panel Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 6/7] add ACME plugin editing Wolfgang Bumiller
2021-03-12 15:24 ` [pmg-devel] [PATCH v2 widget-toolkit 7/7] add ACME domain editing Wolfgang Bumiller
2021-03-15 18:45 ` [pmg-devel] [PATCH v2 api/gui/wtk/acme 0/many] Certificates & ACME Stoiko Ivanov
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20210315182214.25eac96b@rosa.proxmox.com \
--to=s.ivanov@proxmox.com \
--cc=pmg-devel@lists.proxmox.com \
--cc=w.bumiller@proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox