]> git.proxmox.com Git - proxmox-backup.git/commitdiff
gui: add API token UI
authorFabian Grünbichler <f.gruenbichler@proxmox.com>
Thu, 22 Oct 2020 09:40:43 +0000 (11:40 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Thu, 29 Oct 2020 14:14:27 +0000 (15:14 +0100)
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
www/Makefile
www/NavigationTree.js
www/Utils.js
www/config/TokenView.js [new file with mode: 0644]
www/window/TokenEdit.js [new file with mode: 0644]

index cba8bed543da50dc28ecf62f31e448b4b31a49ad..77420a5650facc0a89d445a6cc052a28d8725e14 100644 (file)
@@ -13,6 +13,7 @@ JSSRC=                                                        \
        data/RunningTasksStore.js                       \
        button/TaskButton.js                            \
        config/UserView.js                              \
+       config/TokenView.js                             \
        config/RemoteView.js                            \
        config/ACLView.js                               \
        config/SyncView.js                              \
@@ -27,6 +28,7 @@ JSSRC=                                                        \
        window/SyncJobEdit.js                           \
        window/UserEdit.js                              \
        window/UserPassword.js                          \
+       window/TokenEdit.js                             \
        window/VerifyJobEdit.js                         \
        window/ZFSCreate.js                             \
        dashboard/DataStoreStatistics.js                \
index 6524a5c39612ce03eebed196c9b5c10041d91bfd..d4e5d966b44de61a85029ff413a1f22577732833 100644 (file)
@@ -34,6 +34,12 @@ Ext.define('PBS.store.NavigationStore', {
                        path: 'pbsUserView',
                        leaf: true,
                    },
+                   {
+                       text: gettext('API Token'),
+                       iconCls: 'fa fa-user-o',
+                       path: 'pbsTokenView',
+                       leaf: true,
+                   },
                    {
                        text: gettext('Permissions'),
                        iconCls: 'fa fa-unlock',
index 221a2f2b65d4b05bc0a41afd322a0f3c3cd8fa1e..58319345706be92eed7796feb77adc30db6e6b1f 100644 (file)
@@ -84,6 +84,14 @@ Ext.define('PBS.Utils', {
        return `Datastore ${what} ${id}`;
     },
 
+    extractTokenUser: function(tokenid) {
+       return tokenid.match(/^(.+)!([^!]+)$/)[1];
+    },
+
+    extractTokenName: function(tokenid) {
+       return tokenid.match(/^(.+)!([^!]+)$/)[2];
+    },
+
     constructor: function() {
        var me = this;
 
diff --git a/www/config/TokenView.js b/www/config/TokenView.js
new file mode 100644 (file)
index 0000000..88b3f19
--- /dev/null
@@ -0,0 +1,218 @@
+Ext.define('pbs-tokens', {
+    extend: 'Ext.data.Model',
+    fields: [
+       'tokenid', 'tokenname', 'user', 'comment',
+       { type: 'boolean', name: 'enable', defaultValue: true },
+       { type: 'date', dateFormat: 'timestamp', name: 'expire' },
+    ],
+    idProperty: 'tokenid',
+});
+
+Ext.define('pbs-users-with-tokens', {
+    extend: 'Ext.data.Model',
+    fields: [
+       'userid', 'firstname', 'lastname', 'email', 'comment',
+       { type: 'boolean', name: 'enable', defaultValue: true },
+       { type: 'date', dateFormat: 'timestamp', name: 'expire' },
+       'tokens',
+    ],
+    idProperty: 'userid',
+    proxy: {
+       type: 'proxmox',
+       url: '/api2/json/access/users/?include_tokens=1',
+    },
+});
+
+Ext.define('PBS.config.TokenView', {
+    extend: 'Ext.grid.GridPanel',
+    alias: 'widget.pbsTokenView',
+
+    stateful: true,
+    stateId: 'grid-tokens',
+
+    title: gettext('API Tokens'),
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       init: function(view) {
+           view.userStore = Ext.create('Proxmox.data.UpdateStore', {
+               autoStart: true,
+               interval: 5 * 1000,
+               storeId: 'pbs-users-with-tokens',
+               storeid: 'pbs-users-with-tokens',
+               model: 'pbs-users-with-tokens',
+           });
+           view.userStore.on('load', this.onLoad, this);
+           view.on('destroy', view.userStore.stopUpdate);
+           Proxmox.Utils.monStoreErrors(view, view.userStore);
+       },
+
+       reload: function() { this.getView().userStore.load(); },
+
+       onLoad: function(store, data, success) {
+           if (!success) return;
+
+           let tokenStore = this.getView().store.rstore;
+
+           let records = [];
+           Ext.Array.each(data, function(user) {
+               let tokens = user.data.tokens || [];
+               Ext.Array.each(tokens, function(token) {
+                   let r = {};
+                   r.tokenid = token.tokenid;
+                   r.comment = token.comment;
+                   r.expire = token.expire;
+                   r.enable = token.enable;
+                   records.push(r);
+               });
+           });
+
+           tokenStore.loadData(records);
+           tokenStore.fireEvent('load', tokenStore, records, true);
+       },
+
+       addToken: function() {
+           let me = this;
+           Ext.create('PBS.window.TokenEdit', {
+               isCreate: true,
+               listeners: {
+                   destroy: function() {
+                       me.reload();
+                   },
+               },
+           }).show();
+       },
+
+       editToken: function() {
+           let me = this;
+           let view = me.getView();
+           let selection = view.getSelection();
+           if (selection.length < 1) return;
+           Ext.create('PBS.window.TokenEdit', {
+               user: PBS.Utils.extractTokenUser(selection[0].data.tokenid),
+               tokenname: PBS.Utils.extractTokenName(selection[0].data.tokenid),
+               listeners: {
+                   destroy: function() {
+                       me.reload();
+                   },
+               },
+           }).show();
+       },
+
+       showPermissions: function() {
+           let me = this;
+           let view = me.getView();
+           let selection = view.getSelection();
+
+           if (selection.length < 1) return;
+
+           Ext.create('Proxmox.PermissionView', {
+               auth_id: selection[0].data.tokenid,
+               auth_id_name: 'auth_id',
+           }).show();
+       },
+
+       renderUser: function(tokenid) {
+           return Ext.String.htmlEncode(PBS.Utils.extractTokenUser(tokenid));
+       },
+
+       renderTokenname: function(tokenid) {
+           return Ext.String.htmlEncode(PBS.Utils.extractTokenName(tokenid));
+       },
+
+    },
+
+    listeners: {
+       activate: 'reload',
+       itemdblclick: 'editToken',
+    },
+
+    store: {
+       type: 'diff',
+       autoDestroy: true,
+       autoDestroyRstore: true,
+       sorters: 'tokenid',
+       model: 'pbs-tokens',
+       rstore: {
+           type: 'store',
+           proxy: 'memory',
+           storeid: 'pbs-tokens',
+           model: 'pbs-tokens',
+       },
+    },
+
+    tbar: [
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Add'),
+           handler: 'addToken',
+           selModel: false,
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Edit'),
+           handler: 'editToken',
+           disabled: true,
+       },
+       {
+           xtype: 'proxmoxStdRemoveButton',
+           baseurl: '/access/users/',
+           callback: 'reload',
+           getUrl: function(rec) {
+               let tokenid = rec.getId();
+               let user = PBS.Utils.extractTokenUser(tokenid);
+               let tokenname = PBS.Utils.extractTokenName(tokenid);
+               return '/access/users/' + encodeURIComponent(user) + '/token/' + encodeURIComponent(tokenname);
+           },
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Permissions'),
+           handler: 'showPermissions',
+           disabled: true,
+       },
+    ],
+
+    viewConfig: {
+       trackOver: false,
+    },
+
+    columns: [
+       {
+           header: gettext('User'),
+           width: 200,
+           sortable: true,
+           renderer: 'renderUser',
+           dataIndex: 'tokenid',
+       },
+       {
+           header: gettext('Token name'),
+           width: 100,
+           sortable: true,
+           renderer: 'renderTokenname',
+           dataIndex: 'tokenid',
+       },
+       {
+           header: gettext('Enabled'),
+           width: 80,
+           sortable: true,
+           renderer: Proxmox.Utils.format_boolean,
+           dataIndex: 'enable',
+       },
+       {
+           header: gettext('Expire'),
+           width: 80,
+           sortable: true,
+           renderer: Proxmox.Utils.format_expire,
+           dataIndex: 'expire',
+       },
+       {
+           header: gettext('Comment'),
+           sortable: false,
+           renderer: Ext.String.htmlEncode,
+           dataIndex: 'comment',
+           flex: 1,
+       },
+    ],
+});
diff --git a/www/window/TokenEdit.js b/www/window/TokenEdit.js
new file mode 100644 (file)
index 0000000..6b41ae9
--- /dev/null
@@ -0,0 +1,213 @@
+Ext.define('PBS.window.TokenEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pbsTokenEdit',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'user_mgmt',
+
+    user: undefined,
+    tokenname: undefined,
+
+    isAdd: true,
+    isCreate: false,
+    fixedUser: false,
+
+    subject: gettext('API token'),
+
+    fieldDefaults: { labelWidth: 120 },
+
+    items: {
+       xtype: 'inputpanel',
+       column1: [
+           {
+               xtype: 'pmxDisplayEditField',
+               cbind: {
+                   editable: (get) => get('isCreate') && !get('fixedUser'),
+               },
+               editConfig: {
+                   xtype: 'pbsUserSelector',
+                   allowBlank: false,
+               },
+               name: 'user',
+               value: Proxmox.UserName,
+               renderer: Ext.String.htmlEncode,
+               fieldLabel: gettext('User'),
+           },
+           {
+               xtype: 'pmxDisplayEditField',
+               cbind: {
+                   editable: '{isCreate}',
+               },
+               name: 'tokenname',
+               fieldLabel: gettext('Token Name'),
+               minLength: 2,
+               allowBlank: false,
+           },
+       ],
+
+       column2: [
+           {
+                xtype: 'datefield',
+                name: 'expire',
+               emptyText: Proxmox.Utils.neverText,
+               format: 'Y-m-d',
+               submitFormat: 'U',
+                fieldLabel: gettext('Expire'),
+            },
+           {
+               xtype: 'proxmoxcheckbox',
+               fieldLabel: gettext('Enabled'),
+               name: 'enable',
+               uncheckedValue: 0,
+               defaultValue: 1,
+               checked: true,
+           },
+       ],
+
+       columnB: [
+           {
+               xtype: 'proxmoxtextfield',
+               name: 'comment',
+               fieldLabel: gettext('Comment'),
+           },
+       ],
+    },
+
+    getValues: function(dirtyOnly) {
+       var me = this;
+
+       var values = me.callParent(arguments);
+
+       // hack: ExtJS datefield does not submit 0, so we need to set that
+       if (!values.expire) {
+           values.expire = 0;
+       }
+
+       if (me.isCreate) {
+           me.url = '/api2/extjs/access/users/';
+           let uid = encodeURIComponent(values.user);
+           let tid = encodeURIComponent(values.tokenname);
+           delete values.user;
+           delete values.tokenname;
+
+           me.url += `${uid}/token/${tid}`;
+       }
+
+       return values;
+    },
+
+    setValues: function(values) {
+       var me = this;
+
+       if (Ext.isDefined(values.expire)) {
+           if (values.expire) {
+               values.expire = new Date(values.expire * 1000);
+           } else {
+               // display 'never' instead of '1970-01-01'
+               values.expire = null;
+           }
+       }
+
+       me.callParent([values]);
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       me.url = '/api2/extjs/access/users/';
+
+       me.callParent();
+
+       if (me.isCreate) {
+           me.method = 'POST';
+       } else {
+           me.method = 'PUT';
+
+           let uid = encodeURIComponent(me.user);
+           let tid = encodeURIComponent(me.tokenname);
+
+           me.url += `${uid}/token/${tid}`;
+           me.load({
+               success: function(response, options) {
+                   let values = response.result.data;
+                   values.user = me.user;
+                   values.tokenname = me.tokenname;
+                   me.setValues(values);
+               },
+           });
+       }
+    },
+
+    apiCallDone: function(success, response, options) {
+       let res = response.result.data;
+       if (!success || !res || !res.value) {
+           return;
+       }
+
+       Ext.create('PBS.window.TokenShow', {
+           autoShow: true,
+           tokenid: res.tokenid,
+           secret: res.value,
+       });
+    },
+});
+
+Ext.define('PBS.window.TokenShow', {
+    extend: 'Ext.window.Window',
+    alias: ['widget.pbsTokenShow'],
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    width: 600,
+    modal: true,
+    resizable: false,
+    title: gettext('Token Secret'),
+
+    items: [
+       {
+           xtype: 'container',
+           layout: 'form',
+           bodyPadding: 10,
+           border: false,
+           fieldDefaults: {
+               labelWidth: 100,
+               anchor: '100%',
+            },
+           padding: '0 10 10 10',
+           items: [
+               {
+                   xtype: 'textfield',
+                   fieldLabel: gettext('Token ID'),
+                   cbind: {
+                       value: '{tokenid}',
+                   },
+                   editable: false,
+               },
+               {
+                   xtype: 'textfield',
+                   fieldLabel: gettext('Secret'),
+                   inputId: 'token-secret-value',
+                   cbind: {
+                       value: '{secret}',
+                   },
+                   editable: false,
+               },
+           ],
+       },
+       {
+           xtype: 'component',
+           border: false,
+           padding: '10 10 10 10',
+           userCls: 'pmx-hint',
+           html: gettext('Please record the API token secret - it will only be displayed now'),
+       },
+    ],
+    buttons: [
+       {
+           handler: function(b) {
+               document.getElementById('token-secret-value').select();
+               document.execCommand("copy");
+           },
+           text: gettext('Copy Secret Value'),
+       },
+    ],
+});