diff --git a/src/Makefile b/src/Makefile index f65f91a..30e8fd5 100644 --- a/src/Makefile +++ b/src/Makefile @@ -93,6 +93,7 @@ JSSRC= \ window/TfaEdit.js \ window/NotesEdit.js \ window/ThemeEdit.js \ + window/SyncWindow.js \ node/APT.js \ node/APTRepositories.js \ node/NetworkEdit.js \ diff --git a/src/Schema.js b/src/Schema.js index 372af89..b247b1e 100644 --- a/src/Schema.js +++ b/src/Schema.js @@ -7,6 +7,7 @@ Ext.define('Proxmox.Schema', { // a singleton add: false, edit: false, pwchange: true, + sync: false, }, openid: { name: gettext('OpenID Connect Server'), @@ -15,15 +16,18 @@ Ext.define('Proxmox.Schema', { // a singleton edit: true, tfa: false, pwchange: false, + sync: false, iconCls: 'pmx-itype-icon-openid-logo', }, ldap: { name: gettext('LDAP Server'), ipanel: 'pmxAuthLDAPPanel', + syncipanel: 'pmxAuthLDAPSyncPanel', add: true, edit: true, tfa: true, pwchange: false, + sync: true, }, }, // to add or change existing for product specific ones diff --git a/src/panel/AuthView.js b/src/panel/AuthView.js index 69fe1a5..52b6cac 100644 --- a/src/panel/AuthView.js +++ b/src/panel/AuthView.js @@ -75,6 +75,23 @@ Ext.define('Proxmox.panel.AuthView', { me.openEditWindow(rec.data.type, rec.data.realm); }, + open_sync_window: function() { + let rec = this.getSelection()[0]; + if (!rec) { + return; + } + if (!Proxmox.Schema.authDomains[rec.data.type].sync) { + return; + } + Ext.create('Proxmox.window.SyncWindow', { + type: rec.data.type, + realm: rec.data.realm, + listeners: { + destroy: () => this.reload(), + }, + }).show(); + }, + initComponent: function() { var me = this; @@ -115,6 +132,13 @@ Ext.define('Proxmox.panel.AuthView', { enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].add, callback: () => me.reload(), }, + { + xtype: 'proxmoxButton', + text: gettext('Sync'), + disabled: true, + enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].sync, + handler: () => me.open_sync_window(), + }, ]; if (me.extraButtons) { diff --git a/src/window/AuthEditLDAP.js b/src/window/AuthEditLDAP.js index a44c536..e62b514 100644 --- a/src/window/AuthEditLDAP.js +++ b/src/window/AuthEditLDAP.js @@ -192,3 +192,168 @@ Ext.define('Proxmox.panel.LDAPInputPanel', { }); + +Ext.define('Proxmox.panel.LDAPSyncInputPanel', { + extend: 'Proxmox.panel.InputPanel', + xtype: 'pmxAuthLDAPSyncPanel', + mixins: ['Proxmox.Mixin.CBind'], + + editableAttributes: ['email'], + editableDefaults: ['scope', 'enable-new'], + default_opts: {}, + sync_attributes: {}, + + type: 'ldap', + + // (de)construct the sync-attributes from the list above, + // not touching all others + onGetValues: function(values) { + let me = this; + + me.editableDefaults.forEach((attr) => { + if (values[attr]) { + me.default_opts[attr] = values[attr]; + delete values[attr]; + } else { + delete me.default_opts[attr]; + } + }); + let vanished_opts = []; + ['acl', 'entry', 'properties'].forEach((prop) => { + if (values[`remove-vanished-${prop}`]) { + vanished_opts.push(prop); + } + delete values[`remove-vanished-${prop}`]; + }); + me.default_opts['remove-vanished'] = vanished_opts.join(';'); + + values['sync-defaults-options'] = Proxmox.Utils.printPropertyString(me.default_opts); + me.editableAttributes.forEach((attr) => { + if (values[attr]) { + me.sync_attributes[attr] = values[attr]; + delete values[attr]; + } else { + delete me.sync_attributes[attr]; + } + }); + values['sync-attributes'] = Proxmox.Utils.printPropertyString(me.sync_attributes); + + Proxmox.Utils.delete_if_default(values, 'sync-defaults-options'); + Proxmox.Utils.delete_if_default(values, 'sync-attributes'); + + if (me.isCreate) { + delete values.delete; // on create we cannot delete values + } + + return values; + }, + + setValues: function(values) { + let me = this; + + if (values['sync-attributes']) { + me.sync_attributes = Proxmox.Utils.parsePropertyString(values['sync-attributes']); + delete values['sync-attributes']; + me.editableAttributes.forEach((attr) => { + if (me.sync_attributes[attr]) { + values[attr] = me.sync_attributes[attr]; + } + }); + } + if (values['sync-defaults-options']) { + me.default_opts = Proxmox.Utils.parsePropertyString(values['sync-defaults-options']); + delete values.default_opts; + me.editableDefaults.forEach((attr) => { + if (me.default_opts[attr]) { + values[attr] = me.default_opts[attr]; + } + }); + + if (me.default_opts['remove-vanished']) { + let opts = me.default_opts['remove-vanished'].split(';'); + for (const opt of opts) { + values[`remove-vanished-${opt}`] = 1; + } + } + } + return me.callParent([values]); + }, + + column1: [ + { + xtype: 'proxmoxtextfield', + name: 'email', + fieldLabel: gettext('E-Mail attribute'), + }, + { + xtype: 'displayfield', + value: gettext('Default Sync Options'), + }, + { + xtype: 'proxmoxKVComboBox', + value: '__default__', + deleteEmpty: false, + comboItems: [ + [ + '__default__', + Ext.String.format( + gettext("{0} ({1})"), + Proxmox.Utils.yesText, + Proxmox.Utils.defaultText, + ), + ], + ['true', Proxmox.Utils.yesText], + ['false', Proxmox.Utils.noText], + ], + name: 'enable-new', + fieldLabel: gettext('Enable new users'), + }, + ], + + column2: [ + { + xtype: 'proxmoxtextfield', + name: 'user-classes', + fieldLabel: gettext('User classes'), + deleteEmpty: true, + emptyText: 'inetorgperson, posixaccount, person, user', + autoEl: { + tag: 'div', + 'data-qtip': gettext('Default user classes: inetorgperson, posixaccount, person, user'), + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'filter', + fieldLabel: gettext('User Filter'), + deleteEmpty: true, + }, + ], + + columnB: [ + { + xtype: 'fieldset', + title: gettext('Remove Vanished Options'), + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('ACL'), + name: 'remove-vanished-acl', + boxLabel: gettext('Remove ACLs of vanished users'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Entry'), + name: 'remove-vanished-entry', + boxLabel: gettext('Remove vanished user'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Properties'), + name: 'remove-vanished-properties', + boxLabel: gettext('Remove vanished properties from synced users.'), + }, + ], + }, + ], +}); diff --git a/src/window/SyncWindow.js b/src/window/SyncWindow.js new file mode 100644 index 0000000..449782a --- /dev/null +++ b/src/window/SyncWindow.js @@ -0,0 +1,192 @@ +Ext.define('Proxmox.window.SyncWindow', { + extend: 'Ext.window.Window', + + title: gettext('Realm Sync'), + + width: 600, + bodyPadding: 10, + modal: true, + resizable: false, + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'form': { + validitychange: function(field, valid) { + this.lookup('preview_btn').setDisabled(!valid); + this.lookup('sync_btn').setDisabled(!valid); + }, + }, + 'button': { + click: function(btn) { + this.sync_realm(btn.reference === 'preview_btn'); + }, + }, + }, + + sync_realm: function(is_preview) { + let view = this.getView(); + let ipanel = this.lookup('ipanel'); + let params = ipanel.getValues(); + + let vanished_opts = []; + ['acl', 'entry', 'properties'].forEach((prop) => { + if (params[`remove-vanished-${prop}`]) { + vanished_opts.push(prop); + } + delete params[`remove-vanished-${prop}`]; + }); + if (vanished_opts.length > 0) { + params['remove-vanished'] = vanished_opts.join(';'); + } + + params['dry-run'] = is_preview ? 1 : 0; + Proxmox.Utils.API2Request({ + url: `/access/domains/${view.realm}/sync`, + waitMsgTarget: view, + method: 'POST', + params, + failure: (response) => { + view.show(); + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: (response) => { + view.hide(); + Ext.create('Proxmox.window.TaskViewer', { + upid: response.result.data, + listeners: { + destroy: () => { + if (is_preview) { + view.show(); + } else { + view.close(); + } + }, + }, + }).show(); + }, + }); + }, + }, + + items: [ + { + xtype: 'form', + reference: 'form', + border: false, + fieldDefaults: { + labelWidth: 100, + anchor: '100%', + }, + items: [{ + xtype: 'inputpanel', + reference: 'ipanel', + column1: [ + { + xtype: 'proxmoxKVComboBox', + value: 'true', + deleteEmpty: false, + allowBlank: false, + comboItems: [ + ['true', Proxmox.Utils.yesText], + ['false', Proxmox.Utils.noText], + ], + name: 'enable-new', + fieldLabel: gettext('Enable new'), + }, + ], + + column2: [ + ], + + columnB: [ + { + xtype: 'fieldset', + title: gettext('Remove Vanished Options'), + items: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('ACL'), + name: 'remove-vanished-acl', + boxLabel: gettext('Remove ACLs of vanished users and groups.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Entry'), + name: 'remove-vanished-entry', + boxLabel: gettext('Remove vanished user and group entries.'), + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Properties'), + name: 'remove-vanished-properties', + boxLabel: gettext('Remove vanished properties from synced users.'), + }, + ], + }, + { + xtype: 'displayfield', + reference: 'defaulthint', + value: gettext('Default sync options can be set by editing the realm.'), + userCls: 'pmx-hint', + hidden: true, + }, + ], + }], + }, + ], + + buttons: [ + '->', + { + text: gettext('Preview'), + reference: 'preview_btn', + }, + { + text: gettext('Sync'), + reference: 'sync_btn', + }, + ], + + initComponent: function() { + if (!this.realm) { + throw "no realm defined"; + } + + if (!this.type) { + throw "no realm type defined"; + } + + this.callParent(); + + Proxmox.Utils.API2Request({ + url: `/config/access/${this.type}/${this.realm}`, + waitMsgTarget: this, + method: 'GET', + failure: (response) => { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + this.close(); + }, + success: (response) => { + let default_options = response.result.data['sync-defaults-options']; + if (default_options) { + let options = Proxmox.Utils.parsePropertyString(default_options); + if (options['remove-vanished']) { + let opts = options['remove-vanished'].split(';'); + for (const opt of opts) { + options[`remove-vanished-${opt}`] = 1; + } + } + let ipanel = this.lookup('ipanel'); + ipanel.setValues(options); + } else { + this.lookup('defaulthint').setVisible(true); + } + + // check validity for button state + this.lookup('form').isValid(); + }, + }); + }, +});