[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