[pve-devel] [PATCH widget-toolkit 5/7] add totp, wa and recovery creation and tfa edit windows

Wolfgang Bumiller w.bumiller at proxmox.com
Tue Nov 9 12:27:19 CET 2021


plain copy from pbs with s/pbs/pmx/ and s/PBS/Proxmox/

Signed-off-by: Wolfgang Bumiller <w.bumiller at proxmox.com>
---
 src/Makefile                 |   4 +
 src/window/AddTfaRecovery.js | 224 ++++++++++++++++++++++++++
 src/window/AddTotp.js        | 297 +++++++++++++++++++++++++++++++++++
 src/window/AddWebauthn.js    | 226 ++++++++++++++++++++++++++
 src/window/TfaEdit.js        |  93 +++++++++++
 5 files changed, 844 insertions(+)
 create mode 100644 src/window/AddTfaRecovery.js
 create mode 100644 src/window/AddTotp.js
 create mode 100644 src/window/AddWebauthn.js
 create mode 100644 src/window/TfaEdit.js

diff --git a/src/Makefile b/src/Makefile
index afb0cb2..ad7a3c2 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -79,6 +79,10 @@ JSSRC=					\
 	window/AuthEditBase.js		\
 	window/AuthEditOpenId.js	\
 	window/TfaWindow.js		\
+	window/AddTfaRecovery.js	\
+	window/AddTotp.js		\
+	window/AddWebauthn.js		\
+	window/TfaEdit.js		\
 	node/APT.js			\
 	node/APTRepositories.js		\
 	node/NetworkEdit.js		\
diff --git a/src/window/AddTfaRecovery.js b/src/window/AddTfaRecovery.js
new file mode 100644
index 0000000..174d553
--- /dev/null
+++ b/src/window/AddTfaRecovery.js
@@ -0,0 +1,224 @@
+Ext.define('Proxmox.window.AddTfaRecovery', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pmxAddTfaRecovery',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'user_mgmt',
+    isCreate: true,
+    isAdd: true,
+    subject: gettext('TFA recovery keys'),
+    width: 512,
+    method: 'POST',
+
+    fixedUser: false,
+
+    url: '/api2/extjs/access/tfa',
+    submitUrl: function(url, values) {
+	let userid = values.userid;
+	delete values.userid;
+	return `${url}/${userid}`;
+    },
+
+    apiCallDone: function(success, response) {
+	if (!success) {
+	    return;
+	}
+
+	let values = response
+	    .result
+	    .data
+	    .recovery
+	    .map((v, i) => `${i}: ${v}`)
+	    .join("\n");
+	Ext.create('Proxmox.window.TfaRecoveryShow', {
+	    autoShow: true,
+	    userid: this.getViewModel().get('userid'),
+	    values,
+	});
+    },
+
+    viewModel: {
+	data: {
+	    has_entry: false,
+	    userid: null,
+	},
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+	hasEntry: async function(userid) {
+	    let me = this;
+	    let view = me.getView();
+
+	    try {
+		await Proxmox.Async.api2({
+		    url: `${view.url}/${userid}/recovery`,
+		    method: 'GET',
+		});
+		return true;
+	    } catch (_response) {
+		return false;
+	    }
+	},
+
+	init: function(view) {
+	    this.onUseridChange(null, Proxmox.UserName);
+	},
+
+	onUseridChange: async function(field, userid) {
+	    let me = this;
+	    let vm = me.getViewModel();
+
+	    me.userid = userid;
+	    vm.set('userid', userid);
+
+	    let has_entry = await me.hasEntry(userid);
+	    vm.set('has_entry', has_entry);
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'pmxDisplayEditField',
+	    name: 'userid',
+	    cbind: {
+		editable: (get) => !get('fixedUser'),
+		value: () => Proxmox.UserName,
+	    },
+	    fieldLabel: gettext('User'),
+	    editConfig: {
+		xtype: 'pmxUserSelector',
+		allowBlank: false,
+		validator: function(_value) {
+		    return !this.up('window').getViewModel().get('has_entry');
+		},
+	    },
+	    renderer: Ext.String.htmlEncode,
+	    listeners: {
+		change: 'onUseridChange',
+	    },
+	},
+	{
+	    xtype: 'hiddenfield',
+	    name: 'type',
+	    value: 'recovery',
+	},
+	{
+	    xtype: 'displayfield',
+	    bind: {
+		hidden: '{!has_entry}',
+	    },
+	    hidden: true,
+	    userCls: 'pmx-hint',
+	    value: gettext('User already has recovery keys.'),
+	},
+	{
+	    xtype: 'textfield',
+	    name: 'password',
+	    reference: 'password',
+	    fieldLabel: gettext('Verify Password'),
+	    inputType: 'password',
+	    minLength: 5,
+	    allowBlank: false,
+	    validateBlank: true,
+	    cbind: {
+		hidden: () => Proxmox.UserName === 'root at pam',
+		disabled: () => Proxmox.UserName === 'root at pam',
+		emptyText: () =>
+		    Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
+	    },
+	},
+    ],
+});
+
+Ext.define('Proxmox.window.TfaRecoveryShow', {
+    extend: 'Ext.window.Window',
+    alias: ['widget.pmxTfaRecoveryShow'],
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    width: 600,
+    modal: true,
+    resizable: false,
+    title: gettext('Recovery Keys'),
+    onEsc: Ext.emptyFn,
+
+    items: [
+	{
+	    xtype: 'form',
+	    layout: 'anchor',
+	    bodyPadding: 10,
+	    border: false,
+	    fieldDefaults: {
+		anchor: '100%',
+            },
+	    items: [
+		{
+		    xtype: 'textarea',
+		    editable: false,
+		    inputId: 'token-secret-value',
+		    cbind: {
+			value: '{values}',
+		    },
+		    fieldStyle: {
+			'fontFamily': 'monospace',
+		    },
+		    height: '160px',
+		},
+		{
+		    xtype: 'displayfield',
+		    border: false,
+		    padding: '5 0 0 0',
+		    userCls: 'pmx-hint',
+		    value: gettext('Please record recovery keys - they will only be displayed now'),
+		},
+	    ],
+	},
+    ],
+    buttons: [
+	{
+	    handler: function(b) {
+		document.getElementById('token-secret-value').select();
+		document.execCommand("copy");
+	    },
+	    iconCls: 'fa fa-clipboard',
+	    text: gettext('Copy Recovery Keys'),
+	},
+	{
+	    handler: function(b) {
+		let win = this.up('window');
+		win.paperkeys(win.values, win.userid);
+	    },
+	    iconCls: 'fa fa-print',
+	    text: gettext('Print Recovery Keys'),
+	},
+    ],
+    paperkeys: function(keyString, userid) {
+	let me = this;
+
+	let printFrame = document.createElement("iframe");
+	Object.assign(printFrame.style, {
+	    position: "fixed",
+	    right: "0",
+	    bottom: "0",
+	    width: "0",
+	    height: "0",
+	    border: "0",
+	});
+	const host = document.location.host;
+	const title = document.title;
+	const html = `<html><head><script>
+	    window.addEventListener('DOMContentLoaded', (ev) => window.print());
+	</script><style>@media print and (max-height: 150mm) {
+	  h4, p { margin: 0; font-size: 1em; }
+	}</style></head><body style="padding: 5px;">
+	<h4>Recovery Keys for '${userid}' - ${title} (${host})</h4>
+<p style="font-size:1.5em;line-height:1.5em;font-family:monospace;
+   white-space:pre-wrap;overflow-wrap:break-word;">
+${keyString}
+</p>
+	</body></html>`;
+
+	printFrame.src = "data:text/html;base64," + btoa(html);
+	document.body.appendChild(printFrame);
+    },
+});
diff --git a/src/window/AddTotp.js b/src/window/AddTotp.js
new file mode 100644
index 0000000..3e0f5b5
--- /dev/null
+++ b/src/window/AddTotp.js
@@ -0,0 +1,297 @@
+/*global QRCode*/
+Ext.define('Proxmox.window.AddTotp', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pmxAddTotp',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'user_mgmt',
+
+    modal: true,
+    resizable: false,
+    title: gettext('Add a TOTP login factor'),
+    width: 512,
+    layout: {
+	type: 'vbox',
+	align: 'stretch',
+    },
+
+    isAdd: true,
+    userid: undefined,
+    tfa_id: undefined,
+    issuerName: 'Proxmox',
+    fixedUser: false,
+
+    updateQrCode: function() {
+	let me = this;
+	let values = me.lookup('totp_form').getValues();
+	let algorithm = values.algorithm;
+	if (!algorithm) {
+	    algorithm = 'SHA1';
+	}
+
+	let otpuri =
+	    'otpauth://totp/' +
+	    encodeURIComponent(values.issuer) +
+	    ':' +
+	    encodeURIComponent(values.userid) +
+	    '?secret=' + values.secret +
+	    '&period=' + values.step +
+	    '&digits=' + values.digits +
+	    '&algorithm=' + algorithm +
+	    '&issuer=' + encodeURIComponent(values.issuer);
+
+	me.getController().getViewModel().set('otpuri', otpuri);
+	me.qrcode.makeCode(otpuri);
+	me.lookup('challenge').setVisible(true);
+	me.down('#qrbox').setVisible(true);
+    },
+
+    viewModel: {
+	data: {
+	    valid: false,
+	    secret: '',
+	    otpuri: '',
+	    userid: null,
+	},
+
+	formulas: {
+	    secretEmpty: function(get) {
+		return get('secret').length === 0;
+	    },
+	},
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+	control: {
+	    'field[qrupdate=true]': {
+		change: function() {
+		    this.getView().updateQrCode();
+		},
+	    },
+	    'field': {
+		validitychange: function(field, valid) {
+		    let me = this;
+		    let viewModel = me.getViewModel();
+		    let form = me.lookup('totp_form');
+		    let challenge = me.lookup('challenge');
+		    let password = me.lookup('password');
+		    viewModel.set('valid', form.isValid() && challenge.isValid() && password.isValid());
+		},
+	    },
+	    '#': {
+		show: function() {
+		    let me = this;
+		    let view = me.getView();
+
+		    view.qrdiv = document.createElement('div');
+		    view.qrcode = new QRCode(view.qrdiv, {
+			width: 256,
+			height: 256,
+			correctLevel: QRCode.CorrectLevel.M,
+		    });
+		    view.down('#qrbox').getEl().appendChild(view.qrdiv);
+
+		    view.getController().randomizeSecret();
+		},
+	    },
+	},
+
+	randomizeSecret: function() {
+	    let me = this;
+	    let rnd = new Uint8Array(32);
+	    window.crypto.getRandomValues(rnd);
+	    let data = '';
+	    rnd.forEach(function(b) {
+		// secret must be base32, so just use the first 5 bits
+		b = b & 0x1f;
+		if (b < 26) {
+		    // A..Z
+		    data += String.fromCharCode(b + 0x41);
+		} else {
+		    // 2..7
+		    data += String.fromCharCode(b-26 + 0x32);
+		}
+	    });
+	    me.getViewModel().set('secret', data);
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'form',
+	    layout: 'anchor',
+	    border: false,
+	    reference: 'totp_form',
+	    fieldDefaults: {
+		anchor: '100%',
+	    },
+	    items: [
+		{
+		    xtype: 'pmxDisplayEditField',
+		    name: 'userid',
+		    cbind: {
+			editable: (get) => get('isAdd') && !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);
+			},
+		    },
+		    qrupdate: true,
+		},
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('Description'),
+		    emptyText: gettext('For example: TFA device ID, required to identify multiple factors.'),
+		    allowBlank: false,
+		    name: 'description',
+		    maxLength: 256,
+		},
+		{
+		    layout: 'hbox',
+		    border: false,
+		    padding: '0 0 5 0',
+		    items: [
+			{
+			    xtype: 'textfield',
+			    fieldLabel: gettext('Secret'),
+			    emptyText: gettext('Unchanged'),
+			    name: 'secret',
+			    reference: 'tfa_secret',
+			    regex: /^[A-Z2-7=]+$/,
+			    regexText: 'Must be base32 [A-Z2-7=]',
+			    maskRe: /[A-Z2-7=]/,
+			    qrupdate: true,
+			    bind: {
+				value: "{secret}",
+			    },
+			    flex: 4,
+			    padding: '0 5 0 0',
+			},
+			{
+			    xtype: 'button',
+			    text: gettext('Randomize'),
+			    reference: 'randomize_button',
+			    handler: 'randomizeSecret',
+			    flex: 1,
+			},
+		    ],
+		},
+		{
+		    xtype: 'numberfield',
+		    fieldLabel: gettext('Time period'),
+		    name: 'step',
+		    // Google Authenticator ignores this and generates bogus data
+		    hidden: true,
+		    value: 30,
+		    minValue: 10,
+		    qrupdate: true,
+		},
+		{
+		    xtype: 'numberfield',
+		    fieldLabel: gettext('Digits'),
+		    name: 'digits',
+		    value: 6,
+		    // Google Authenticator ignores this and generates bogus data
+		    hidden: true,
+		    minValue: 6,
+		    maxValue: 8,
+		    qrupdate: true,
+		},
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('Issuer Name'),
+		    name: 'issuer',
+		    cbind: {
+			value: '{issuerName}',
+		    },
+		    qrupdate: true,
+		},
+		{
+		    xtype: 'box',
+		    itemId: 'qrbox',
+		    visible: false, // will be enabled when generating a qr code
+		    bind: {
+			visible: '{!secretEmpty}',
+		    },
+		    style: {
+			'background-color': 'white',
+			'margin-left': 'auto',
+			'margin-right': 'auto',
+			padding: '5px',
+			width: '266px',
+			height: '266px',
+		    },
+		},
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('Verify Code'),
+		    allowBlank: false,
+		    reference: 'challenge',
+		    name: 'challenge',
+		    bind: {
+			disabled: '{!showTOTPVerifiction}',
+			visible: '{showTOTPVerifiction}',
+		    },
+		    emptyText: gettext('Scan QR code in a TOTP app and enter an auth. code here'),
+		},
+		{
+		    xtype: 'textfield',
+		    name: 'password',
+		    reference: 'password',
+		    fieldLabel: gettext('Verify Password'),
+		    inputType: 'password',
+		    minLength: 5,
+		    allowBlank: false,
+		    validateBlank: true,
+		    cbind: {
+			hidden: () => Proxmox.UserName === 'root at pam',
+			disabled: () => Proxmox.UserName === 'root at pam',
+			emptyText: () =>
+			    Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
+		    },
+		},
+	    ],
+	},
+    ],
+
+    initComponent: function() {
+	let me = this;
+	me.url = '/api2/extjs/access/tfa/';
+	me.method = 'POST';
+	me.callParent();
+    },
+
+    getValues: function(dirtyOnly) {
+	let me = this;
+	let viewmodel = me.getController().getViewModel();
+
+	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: "totp",
+	    totp: viewmodel.get('otpuri'),
+	    value: values.challenge,
+	};
+
+	if (values.password) {
+	    data.password = values.password;
+	}
+
+	return data;
+    },
+});
diff --git a/src/window/AddWebauthn.js b/src/window/AddWebauthn.js
new file mode 100644
index 0000000..f4a0b10
--- /dev/null
+++ b/src/window/AddWebauthn.js
@@ -0,0 +1,226 @@
+Ext.define('Proxmox.window.AddWebauthn', {
+    extend: 'Ext.window.Window',
+    alias: 'widget.pmxAddWebauthn',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'user_mgmt',
+
+    modal: true,
+    resizable: false,
+    title: gettext('Add a Webauthn login token'),
+    width: 512,
+
+    user: undefined,
+    fixedUser: false,
+
+    initComponent: function() {
+	let me = this;
+	me.callParent();
+	Ext.GlobalEvents.fireEvent('proxmoxShowHelp', me.onlineHelp);
+    },
+
+    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('webauthn_form');
+		    viewmodel.set('valid', form.isValid());
+		},
+	    },
+	    '#': {
+		show: function() {
+		    let me = this;
+		    let view = me.getView();
+
+		    if (Proxmox.UserName === 'root at pam') {
+			view.lookup('password').setVisible(false);
+			view.lookup('password').setDisabled(true);
+		    }
+		},
+	    },
+	},
+
+	registerWebauthn: async function() {
+	    let me = this;
+	    let values = me.lookup('webauthn_form').getValues();
+	    values.type = "webauthn";
+
+	    let userid = values.user;
+	    delete values.user;
+
+	    me.getView().mask(gettext('Please wait...'), 'x-mask-loading');
+
+	    try {
+		let register_response = await Proxmox.Async.api2({
+		    url: `/api2/extjs/access/tfa/${userid}`,
+		    method: 'POST',
+		    params: values,
+		});
+
+		let data = register_response.result.data;
+		if (!data.challenge) {
+		    throw "server did not respond with a challenge";
+		}
+
+		let creds = JSON.parse(data.challenge);
+
+		// Fix this up before passing it to the browser, but keep a copy of the original
+		// string to pass in the response:
+		let challenge_str = creds.publicKey.challenge;
+		creds.publicKey.challenge = Proxmox.Utils.base64url_to_bytes(challenge_str);
+		creds.publicKey.user.id =
+		    Proxmox.Utils.base64url_to_bytes(creds.publicKey.user.id);
+
+		// convert existing authenticators structure
+		creds.publicKey.excludeCredentials =
+		    (creds.publicKey.excludeCredentials || [])
+		    .map((credential) => ({
+			id: Proxmox.Utils.base64url_to_bytes(credential.id),
+			type: credential.type,
+		    }));
+
+		let msg = Ext.Msg.show({
+		    title: `Webauthn: ${gettext('Setup')}`,
+		    message: gettext('Please press the button on your Webauthn Device'),
+		    buttons: [],
+		});
+
+		let token_response;
+		try {
+		    token_response = await navigator.credentials.create(creds);
+		} catch (error) {
+		    let errmsg = error.message;
+		    if (error.name === 'InvalidStateError') {
+			errmsg = gettext('Is this token already registered?');
+		    }
+		    throw gettext('An error occurred during token registration.') +
+			`<br>${error.name}: ${errmsg}`;
+		}
+
+		// We cannot pass ArrayBuffers to the API, so extract & convert the data.
+		let response = {
+		    id: token_response.id,
+		    type: token_response.type,
+		    rawId: Proxmox.Utils.bytes_to_base64url(token_response.rawId),
+		    response: {
+			attestationObject: Proxmox.Utils.bytes_to_base64url(
+			    token_response.response.attestationObject,
+			),
+			clientDataJSON: Proxmox.Utils.bytes_to_base64url(
+			    token_response.response.clientDataJSON,
+			),
+		    },
+		};
+
+		msg.close();
+
+		let params = {
+		    type: "webauthn",
+		    challenge: challenge_str,
+		    value: JSON.stringify(response),
+		};
+
+		if (values.password) {
+		    params.password = values.password;
+		}
+
+		await Proxmox.Async.api2({
+		    url: `/api2/extjs/access/tfa/${userid}`,
+		    method: 'POST',
+		    params,
+		});
+	    } catch (response) {
+		let error = response.result.message;
+		console.error(error); // for debugging if it's not displayable...
+		Ext.Msg.alert(gettext('Error'), error);
+	    }
+
+	    me.getView().close();
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'form',
+	    reference: 'webauthn_form',
+	    layout: 'anchor',
+	    border: false,
+	    bodyPadding: 10,
+	    fieldDefaults: {
+		anchor: '100%',
+	    },
+	    items: [
+		{
+		    xtype: 'pmxDisplayEditField',
+		    name: 'user',
+		    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',
+		    name: 'password',
+		    reference: 'password',
+		    fieldLabel: gettext('Verify Password'),
+		    inputType: 'password',
+		    minLength: 5,
+		    allowBlank: false,
+		    validateBlank: true,
+		    cbind: {
+			hidden: () => Proxmox.UserName === 'root at pam',
+			disabled: () => Proxmox.UserName === 'root at pam',
+			emptyText: () =>
+			    Ext.String.format(gettext("Confirm your ({0}) password"), Proxmox.UserName),
+		    },
+		},
+	    ],
+	},
+    ],
+
+    buttons: [
+	{
+	    xtype: 'proxmoxHelpButton',
+	},
+	'->',
+	{
+	    xtype: 'button',
+	    text: gettext('Register Webauthn Device'),
+	    handler: 'registerWebauthn',
+	    bind: {
+		disabled: '{!valid}',
+	    },
+	},
+    ],
+});
diff --git a/src/window/TfaEdit.js b/src/window/TfaEdit.js
new file mode 100644
index 0000000..710f2b9
--- /dev/null
+++ b/src/window/TfaEdit.js
@@ -0,0 +1,93 @@
+Ext.define('Proxmox.window.TfaEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pmxTfaEdit',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'user_mgmt',
+
+    modal: true,
+    resizable: false,
+    title: gettext("Modify a TFA entry's description"),
+    width: 512,
+
+    layout: {
+	type: 'vbox',
+	align: 'stretch',
+    },
+
+    cbindData: function(initialConfig) {
+	let me = this;
+
+	let tfa_id = initialConfig['tfa-id'];
+	me.tfa_id = tfa_id;
+	me.defaultFocus = 'textfield[name=description]';
+	me.url = `/api2/extjs/access/tfa/${tfa_id}`;
+	me.method = 'PUT';
+	me.autoLoad = true;
+	return {};
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.callParent();
+
+	if (Proxmox.UserName === 'root at pam') {
+	    me.lookup('password').setVisible(false);
+	    me.lookup('password').setDisabled(true);
+	}
+
+	let userid = me.tfa_id.split('/')[0];
+	me.lookup('userid').setValue(userid);
+    },
+
+    items: [
+	{
+	    xtype: 'displayfield',
+	    reference: 'userid',
+	    editable: false,
+	    fieldLabel: gettext('User'),
+	    editConfig: {
+		xtype: 'pmxUserSelector',
+		allowBlank: false,
+	    },
+	    cbind: {
+		value: () => Proxmox.UserName,
+	    },
+	},
+	{
+	    xtype: 'proxmoxtextfield',
+	    name: 'description',
+	    allowBlank: false,
+	    fieldLabel: gettext('Description'),
+	},
+	{
+	    xtype: 'proxmoxcheckbox',
+	    fieldLabel: gettext('Enabled'),
+	    name: 'enable',
+	    uncheckedValue: 0,
+	    defaultValue: 1,
+	    checked: true,
+	},
+	{
+	    xtype: 'textfield',
+	    inputType: 'password',
+	    fieldLabel: gettext('Password'),
+	    minLength: 5,
+	    reference: 'password',
+	    name: 'password',
+	    allowBlank: false,
+	    validateBlank: true,
+	    emptyText: gettext('verify current password'),
+	},
+    ],
+
+    getValues: function() {
+	var me = this;
+
+	var values = me.callParent(arguments);
+
+	delete values.userid;
+
+	return values;
+    },
+});
-- 
2.30.2






More information about the pve-devel mailing list