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 C42437D71F for ; Tue, 9 Nov 2021 12:27:53 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id CC0FFB089 for ; Tue, 9 Nov 2021 12:27:34 +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 BE48EB00B for ; Tue, 9 Nov 2021 12:27:28 +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 94D5142740 for ; Tue, 9 Nov 2021 12:27:28 +0100 (CET) From: Wolfgang Bumiller To: pve-devel@lists.proxmox.com Date: Tue, 9 Nov 2021 12:27:21 +0100 Message-Id: <20211109112721.130935-33-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.516 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 Subject: [pve-devel] [PATCH widget-toolkit 7/7] add yubico otp windows & login support 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:27:54 -0000 has to be explicitly enabled since this is only supported in PVE Signed-off-by: Wolfgang Bumiller --- src/Makefile | 1 + src/panel/TfaView.js | 32 +++++++++ src/window/AddYubico.js | 148 ++++++++++++++++++++++++++++++++++++++++ src/window/TfaWindow.js | 31 ++++++++- 4 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/window/AddYubico.js diff --git a/src/Makefile b/src/Makefile index bf2eab0..d9d12e8 100644 --- a/src/Makefile +++ b/src/Makefile @@ -83,6 +83,7 @@ JSSRC= \ window/AddTfaRecovery.js \ window/AddTotp.js \ window/AddWebauthn.js \ + window/AddYubico.js \ window/TfaEdit.js \ node/APT.js \ node/APTRepositories.js \ diff --git a/src/panel/TfaView.js b/src/panel/TfaView.js index a0cb04a..712cdfe 100644 --- a/src/panel/TfaView.js +++ b/src/panel/TfaView.js @@ -18,11 +18,20 @@ Ext.define('pmx-tfa-entry', { Ext.define('Proxmox.panel.TfaView', { extend: 'Ext.grid.GridPanel', alias: 'widget.pmxTfaView', + mixins: ['Proxmox.Mixin.CBind'], title: gettext('Second Factors'), reference: 'tfaview', issuerName: 'Proxmox', + yubicoEnabled: false, + + cbindData: function(initialConfig) { + let me = this; + return { + yubicoEnabled: me.yubicoEnabled, + }; + }, store: { type: 'diff', @@ -116,6 +125,19 @@ Ext.define('Proxmox.panel.TfaView', { }).show(); }, + addYubico: function() { + let me = this; + + Ext.create('Proxmox.window.AddYubico', { + isCreate: true, + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + editItem: function() { let me = this; let view = me.getView(); @@ -227,6 +249,7 @@ Ext.define('Proxmox.panel.TfaView', { tbar: [ { text: gettext('Add'), + cbind: {}, menu: { xtype: 'menu', items: [ @@ -248,6 +271,15 @@ Ext.define('Proxmox.panel.TfaView', { iconCls: 'fa fa-fw fa-file-text-o', handler: 'addRecovery', }, + { + text: gettext('Yubico'), + itemId: 'yubico', + iconCls: 'fa fa-fw fa-yahoo', + handler: 'addYubico', + cbind: { + hidden: '{!yubicoEnabled}', + }, + }, ], }, }, diff --git a/src/window/AddYubico.js b/src/window/AddYubico.js new file mode 100644 index 0000000..22b884b --- /dev/null +++ b/src/window/AddYubico.js @@ -0,0 +1,148 @@ +Ext.define('Proxmox.window.AddYubico', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pmxAddYubico', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'user_mgmt', + + modal: true, + resizable: false, + title: gettext('Add a Yubico key'), + width: 512, + + isAdd: true, + userid: undefined, + fixedUser: false, + + initComponent: function() { + let me = this; + me.url = '/api2/extjs/access/tfa/'; + me.method = 'POST'; + me.callParent(); + }, + + viewModel: { + data: { + valid: false, + userid: null, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field': { + validitychange: function(field, valid) { + let me = this; + let viewmodel = me.getViewModel(); + let form = me.lookup('yubico_form'); + viewmodel.set('valid', form.isValid()); + }, + }, + '#': { + show: function() { + let me = this; + let view = me.getView(); + + if (Proxmox.UserName === 'root@pam') { + view.lookup('password').setVisible(false); + view.lookup('password').setDisabled(true); + } + }, + }, + }, + }, + + items: [ + { + xtype: 'form', + reference: 'yubico_form', + layout: 'anchor', + border: false, + bodyPadding: 10, + fieldDefaults: { + anchor: '100%', + }, + items: [ + { + xtype: 'pmxDisplayEditField', + name: 'userid', + cbind: { + editable: (get) => !get('fixedUser'), + value: () => Proxmox.UserName, + }, + fieldLabel: gettext('User'), + editConfig: { + xtype: 'pmxUserSelector', + allowBlank: false, + }, + renderer: Ext.String.htmlEncode, + listeners: { + change: function(field, newValue, oldValue) { + let vm = this.up('window').getViewModel(); + vm.set('userid', newValue); + }, + }, + }, + { + xtype: 'textfield', + fieldLabel: gettext('Description'), + allowBlank: false, + name: 'description', + maxLength: 256, + emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'), + }, + { + xtype: 'textfield', + fieldLabel: gettext('Yubico OTP Key'), + emptyText: gettext('A currently valid Yubico OTP value'), + name: 'otp_value', + maxLength: 44, + enforceMaxLength: true, + regex: /^[a-zA-Z0-9]{44}$/, + regexText: '44 characters', + maskRe: /^[a-zA-Z0-9]$/, + }, + { + xtype: 'textfield', + name: 'password', + reference: 'password', + fieldLabel: gettext('Verify Password'), + inputType: 'password', + minLength: 5, + allowBlank: false, + validateBlank: true, + cbind: { + hidden: () => Proxmox.UserName === 'root@pam', + disabled: () => Proxmox.UserName === 'root@pam', + emptyText: () => + Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName), + }, + }, + ], + }, + ], + + getValues: function(dirtyOnly) { + let me = this; + + let values = me.callParent(arguments); + + let uid = encodeURIComponent(values.userid); + me.url = `/api2/extjs/access/tfa/${uid}`; + delete values.userid; + + let data = { + description: values.description, + type: "yubico", + value: values.otp_value, + }; + + if (values.password) { + data.password = values.password; + } + + return data; + }, +}); diff --git a/src/window/TfaWindow.js b/src/window/TfaWindow.js index 5026fb8..d568f9b 100644 --- a/src/window/TfaWindow.js +++ b/src/window/TfaWindow.js @@ -45,7 +45,7 @@ Ext.define('Proxmox.window.TfaLoginWindow', { let lastTabId = me.getLastTabUsed(); let initialTab = -1, i = 0; - for (const k of ['webauthn', 'totp', 'recovery', 'u2f']) { + for (const k of ['webauthn', 'totp', 'recovery', 'u2f', 'yubico']) { const available = !!challenge[k]; vm.set(`availableChallenge.${k}`, available); @@ -143,6 +143,13 @@ Ext.define('Proxmox.window.TfaLoginWindow', { let _promise = me.finishChallenge(`totp:${code}`); }, + loginYubico: function() { + let me = this; + + let code = me.lookup('yubico').getValue(); + let _promise = me.finishChallenge(`yubico:${code}`); + }, + loginWebauthn: async function() { let me = this; let view = me.getView(); @@ -412,6 +419,28 @@ Ext.define('Proxmox.window.TfaLoginWindow', { }, ], }, + { + xtype: 'panel', + title: gettext('Yubico OTP'), + iconCls: 'fa fa-fw fa-yahoo', + handler: 'loginYubico', + bind: { + disabled: '{!availableChallenge.yubico}', + }, + items: [ + { + xtype: 'textfield', + fieldLabel: gettext('Please enter your Yubico OTP code'), + labelWidth: 300, + name: 'yubico', + disabled: true, + reference: 'yubico', + allowBlank: false, + regex: /^[a-z0-9]{30,60}$/, // *should* be 44 but not sure if that's "fixed" + regexText: gettext('TOTP codes consist of six decimal digits'), + }, + ], + }, ], }], -- 2.30.2