From 20b39dd8f60983d34d26c62e5ea26afa048391db Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 9 Nov 2021 12:27:21 +0100 Subject: [PATCH] add yubico otp windows & login support 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 5e47bf4..c245a53 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'), + }, + ], + }, ], }],