mirror of
https://git.proxmox.com/git/proxmox-widget-toolkit
synced 2025-05-26 04:08:51 +00:00
add totp, wa and recovery creation and tfa edit windows
plain copy from pbs with s/pbs/pmx/ and s/PBS/Proxmox/ Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
9e1f1ef616
commit
641764474d
@ -79,6 +79,10 @@ JSSRC= \
|
|||||||
window/AuthEditBase.js \
|
window/AuthEditBase.js \
|
||||||
window/AuthEditOpenId.js \
|
window/AuthEditOpenId.js \
|
||||||
window/TfaWindow.js \
|
window/TfaWindow.js \
|
||||||
|
window/AddTfaRecovery.js \
|
||||||
|
window/AddTotp.js \
|
||||||
|
window/AddWebauthn.js \
|
||||||
|
window/TfaEdit.js \
|
||||||
node/APT.js \
|
node/APT.js \
|
||||||
node/APTRepositories.js \
|
node/APTRepositories.js \
|
||||||
node/NetworkEdit.js \
|
node/NetworkEdit.js \
|
||||||
|
224
src/window/AddTfaRecovery.js
Normal file
224
src/window/AddTfaRecovery.js
Normal file
@ -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@pam',
|
||||||
|
disabled: () => Proxmox.UserName === 'root@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);
|
||||||
|
},
|
||||||
|
});
|
297
src/window/AddTotp.js
Normal file
297
src/window/AddTotp.js
Normal file
@ -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@pam',
|
||||||
|
disabled: () => Proxmox.UserName === 'root@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;
|
||||||
|
},
|
||||||
|
});
|
226
src/window/AddWebauthn.js
Normal file
226
src/window/AddWebauthn.js
Normal file
@ -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@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@pam',
|
||||||
|
disabled: () => Proxmox.UserName === 'root@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}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
93
src/window/TfaEdit.js
Normal file
93
src/window/TfaEdit.js
Normal file
@ -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@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;
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user