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)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id CCE117D8A3 for ; Tue, 9 Nov 2021 12:28:23 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 38467B08A for ; Tue, 9 Nov 2021 12:27:37 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 7E9D4B022 for ; Tue, 9 Nov 2021 12:27:29 +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 48CE042669 for ; Tue, 9 Nov 2021 12:27:29 +0100 (CET) From: Wolfgang Bumiller To: pve-devel@lists.proxmox.com Date: Tue, 9 Nov 2021 12:27:20 +0100 Message-Id: <20211109112721.130935-32-w.bumiller@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20211109112721.130935-1-w.bumiller@proxmox.com> References: <20211109112721.130935-1-w.bumiller@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.514 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [entry.id, record.id, user.id] Subject: [pve-devel] [PATCH widget-toolkit 6/7] add Proxmox.panel.TfaView X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 09 Nov 2021 11:28:24 -0000 copied from pbs with s/pbs/pmx/ and s/PBS/Proxmox/ DELETE call changed from using a body to url parameters, since pve doesn't support a body there currently, and pbs doesn't care Signed-off-by: Wolfgang Bumiller --- src/Makefile | 1 + src/panel/TfaView.js | 270 ++++++++++++++++++++++++++++++++++++++++++ src/window/TfaEdit.js | 135 +++++++++++++++++++++ 3 files changed, 406 insertions(+) create mode 100644 src/panel/TfaView.js diff --git a/src/Makefile b/src/Makefile index ad7a3c2..bf2eab0 100644 --- a/src/Makefile +++ b/src/Makefile @@ -63,6 +63,7 @@ JSSRC= \ panel/ACMEPlugin.js \ panel/ACMEDomains.js \ panel/StatusView.js \ + panel/TfaView.js \ window/Edit.js \ window/PasswordEdit.js \ window/SafeDestroy.js \ diff --git a/src/panel/TfaView.js b/src/panel/TfaView.js new file mode 100644 index 0000000..a0cb04a --- /dev/null +++ b/src/panel/TfaView.js @@ -0,0 +1,270 @@ +Ext.define('pmx-tfa-users', { + extend: 'Ext.data.Model', + fields: ['userid'], + idProperty: 'userid', + proxy: { + type: 'proxmox', + url: '/api2/json/access/tfa', + }, +}); + +Ext.define('pmx-tfa-entry', { + extend: 'Ext.data.Model', + fields: ['fullid', 'userid', 'type', 'description', 'created', 'enable'], + idProperty: 'fullid', +}); + + +Ext.define('Proxmox.panel.TfaView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pmxTfaView', + + title: gettext('Second Factors'), + reference: 'tfaview', + + issuerName: 'Proxmox', + + store: { + type: 'diff', + autoDestroy: true, + autoDestroyRstore: true, + model: 'pmx-tfa-entry', + rstore: { + type: 'store', + proxy: 'memory', + storeid: 'pmx-tfa-entry', + model: 'pmx-tfa-entry', + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + let me = this; + view.tfaStore = Ext.create('Proxmox.data.UpdateStore', { + autoStart: true, + interval: 5 * 1000, + storeid: 'pmx-tfa-users', + model: 'pmx-tfa-users', + }); + view.tfaStore.on('load', this.onLoad, this); + view.on('destroy', view.tfaStore.stopUpdate); + Proxmox.Utils.monStoreErrors(view, view.tfaStore); + }, + + reload: function() { this.getView().tfaStore.load(); }, + + onLoad: function(store, data, success) { + if (!success) return; + + let records = []; + Ext.Array.each(data, user => { + Ext.Array.each(user.data.entries, entry => { + records.push({ + fullid: `${user.id}/${entry.id}`, + userid: user.id, + type: entry.type, + description: entry.description, + created: entry.created, + enable: entry.enable, + }); + }); + }); + + let rstore = this.getView().store.rstore; + rstore.loadData(records); + rstore.fireEvent('load', rstore, records, true); + }, + + addTotp: function() { + let me = this; + + Ext.create('Proxmox.window.AddTotp', { + isCreate: true, + issuerName: me.getView().issuerName, + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + addWebauthn: function() { + let me = this; + + Ext.create('Proxmox.window.AddWebauthn', { + isCreate: true, + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + addRecovery: async function() { + let me = this; + + Ext.create('Proxmox.window.AddTfaRecovery', { + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + editItem: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length !== 1 || selection[0].id.endsWith("/recovery")) { + return; + } + + Ext.create('Proxmox.window.TfaEdit', { + 'tfa-id': selection[0].data.fullid, + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + renderUser: fullid => fullid.split('/')[0], + + renderEnabled: enabled => { + if (enabled === undefined) { + return Proxmox.Utils.yesText; + } else { + return Proxmox.Utils.format_boolean(enabled); + } + }, + + onRemoveButton: function(btn, event, record) { + let me = this; + + Ext.create('Proxmox.tfa.confirmRemove', { + ...record.data, + callback: password => me.removeItem(password, record), + }) + .show(); + }, + + removeItem: async function(password, record) { + let me = this; + + if (password !== null) { + password = '?password=' + encodeURIComponent(password); + } else { + password = ''; + } + + try { + me.getView().mask(gettext('Please wait...'), 'x-mask-loading'); + await Proxmox.Async.api2({ + url: `/api2/extjs/access/tfa/${record.id}${password}`, + method: 'DELETE', + }); + me.reload(); + } catch (response) { + Ext.Msg.alert(gettext('Error'), response.result.message); + } finally { + me.getView().unmask(); + } + }, + }, + + viewConfig: { + trackOver: false, + }, + + listeners: { + itemdblclick: 'editItem', + }, + + columns: [ + { + header: gettext('User'), + width: 200, + sortable: true, + dataIndex: 'fullid', + renderer: 'renderUser', + }, + { + header: gettext('Enabled'), + width: 80, + sortable: true, + dataIndex: 'enable', + renderer: 'renderEnabled', + }, + { + header: gettext('TFA Type'), + width: 80, + sortable: true, + dataIndex: 'type', + }, + { + header: gettext('Created'), + width: 150, + sortable: true, + dataIndex: 'created', + renderer: Proxmox.Utils.render_timestamp, + }, + { + header: gettext('Description'), + width: 300, + sortable: true, + dataIndex: 'description', + renderer: Ext.String.htmlEncode, + flex: 1, + }, + ], + + tbar: [ + { + text: gettext('Add'), + menu: { + xtype: 'menu', + items: [ + { + text: gettext('TOTP'), + itemId: 'totp', + iconCls: 'fa fa-fw fa-clock-o', + handler: 'addTotp', + }, + { + text: gettext('Webauthn'), + itemId: 'webauthn', + iconCls: 'fa fa-fw fa-shield', + handler: 'addWebauthn', + }, + { + text: gettext('Recovery Keys'), + itemId: 'recovery', + iconCls: 'fa fa-fw fa-file-text-o', + handler: 'addRecovery', + }, + ], + }, + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + handler: 'editItem', + enableFn: rec => !rec.id.endsWith("/recovery"), + disabled: true, + }, + { + xtype: 'proxmoxButton', + disabled: true, + text: gettext('Remove'), + getRecordName: rec => rec.data.description, + handler: 'onRemoveButton', + }, + ], +}); diff --git a/src/window/TfaEdit.js b/src/window/TfaEdit.js index 710f2b9..4a8b937 100644 --- a/src/window/TfaEdit.js +++ b/src/window/TfaEdit.js @@ -91,3 +91,138 @@ Ext.define('Proxmox.window.TfaEdit', { return values; }, }); + +Ext.define('Proxmox.tfa.confirmRemove', { + extend: 'Proxmox.window.Edit', + mixins: ['Proxmox.Mixin.CBind'], + + title: gettext("Confirm TFA Removal"), + + modal: true, + resizable: false, + width: 600, + isCreate: true, // logic + isRemove: true, + + url: '/access/tfa', + + initComponent: function() { + let me = this; + + if (typeof me.type !== "string") { + throw "missing type"; + } + + if (!me.callback) { + throw "missing callback"; + } + + me.callParent(); + + if (Proxmox.UserName === 'root@pam') { + me.lookup('password').setVisible(false); + me.lookup('password').setDisabled(true); + } + }, + + submit: function() { + let me = this; + if (Proxmox.UserName === 'root@pam') { + me.callback(null); + } else { + me.callback(me.lookup('password').getValue()); + } + me.close(); + }, + + items: [ + { + xtype: 'box', + padding: '0 0 10 0', + html: Ext.String.format( + gettext('Are you sure you want to remove this {0} entry?'), + 'TFA', + ), + }, + { + xtype: 'container', + layout: { + type: 'hbox', + align: 'begin', + }, + defaults: { + border: false, + layout: 'anchor', + flex: 1, + padding: 5, + }, + items: [ + { + xtype: 'container', + layout: { + type: 'vbox', + }, + padding: '0 10 0 0', + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('User'), + cbind: { + value: '{userid}', + }, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Type'), + cbind: { + value: '{type}', + }, + }, + ], + }, + { + xtype: 'container', + layout: { + type: 'vbox', + }, + padding: '0 0 0 10', + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Created'), + renderer: v => Proxmox.Utils.render_timestamp(v), + cbind: { + value: '{created}', + }, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Description'), + cbind: { + value: '{description}', + }, + emptyText: Proxmox.Utils.NoneText, + submitValue: false, + editable: false, + }, + ], + }, + ], + }, + { + xtype: 'textfield', + inputType: 'password', + fieldLabel: gettext('Password'), + minLength: 5, + reference: 'password', + name: 'password', + allowBlank: false, + validateBlank: true, + padding: '10 0 0 0', + cbind: { + emptyText: () => + Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName), + }, + }, + ], +}); -- 2.30.2