From 917dd0835af879987d831abca2480428b9ad32d8 Mon Sep 17 00:00:00 2001 From: Vitaly Kramskikh Date: Wed, 28 Sep 2016 16:53:17 +0300 Subject: [PATCH] Use Keystone V3 API This commit introduces support of V3 API and also removes token regeneration every 1 hour which is totally unnecessary. Also, token is now only stored in User model and passed to the Keystone client via arguments, so there is no more two sources of truth. User id and roles are also stored in User model now. Partial-Bug: #1628445 Closes-Bug: #1618172 Closes-Bug: #1656822 Depends-On: If201c247210131ce6ab192362eada250a4f51ce1 Change-Id: I48b73a09cad0d707c16df5ca8ada202173779129 --- static/app.js | 74 +++++----- static/consts.js | 5 + static/keystone_client.js | 154 ++++++++++----------- static/views/cluster_page_tabs/logs_tab.js | 2 +- static/views/controls.js | 2 +- static/views/dialogs.js | 31 ++++- static/views/login_page.js | 33 +++-- 7 files changed, 174 insertions(+), 127 deletions(-) diff --git a/static/app.js b/static/app.js index ea212e016..36965d635 100644 --- a/static/app.js +++ b/static/app.js @@ -170,11 +170,7 @@ class App { this.statistics = new models.NodesStatistics(); this.notifications = new models.Notifications(); this.releases = new models.Releases(); - this.keystoneClient = new KeystoneClient('/keystone', { - cacheTokenFor: 10 * 60 * 1000, - tenant: 'admin', - token: this.user.get('token') - }); + this.keystoneClient = new KeystoneClient('/keystone'); } initialize() { @@ -183,28 +179,41 @@ class App { document.title = i18n('common.title'); + this.version.set({auth_required: true}); + this.user.set({authenticated: false}); + + var isNailgunAvailable = true; + return this.version.fetch() .then(null, (response) => { - if (response.status === 401) { - this.version.set({auth_required: true}); + if (response.status !== 401) { + isNailgunAvailable = false; + } + return $.Deferred().reject(response); + }) + .then(() => { + this.user.set({authenticated: true}); + if (this.version.get('auth_required')) { + return this.keystoneClient.getTokenInfo(this.user.get('token')) + .done((tokenInfo) => { + this.user.set({ + id: tokenInfo.token.user.id, + roles: tokenInfo.token.roles + }); + }) + .fail(() => { + this.user.set({authenticated: false}); + this.user.unset('token'); + this.user.unset('username'); + return $.Deferred().reject(); + }); + } else { return $.Deferred().resolve(); } }) - .then(() => { - this.user.set({authenticated: !this.version.get('auth_required')}); - if (this.version.get('auth_required')) { - this.keystoneClient.token = this.user.get('token'); - return this.keystoneClient.authenticate() - .then(() => { - this.user.set({authenticated: true}); - return this.version.fetch({cache: true}); - }); - } - return $.Deferred().resolve(); - }) .then(() => this.fuelSettings.fetch()) .then(null, () => { - if (this.version.get('auth_required') && !this.user.get('authenticated')) { + if (isNailgunAvailable) { return $.Deferred().resolve(); } else { this.mountNode.empty(); @@ -246,11 +255,15 @@ class App { logout() { if (this.user.get('authenticated') && this.version.get('auth_required')) { - this.user.set('authenticated', false); - this.user.unset('username'); - this.user.unset('token'); + var token = this.user.get('token'); - this.keystoneClient.deauthenticate(); + this.user.set('authenticated', false); + this.user.unset('token'); + this.user.unset('username'); + this.user.unset('id'); + this.user.unset('roles'); + + this.keystoneClient.deauthenticate(token); } _.defer(() => this.navigate('login', {trigger: true, replace: true})); @@ -265,15 +278,10 @@ class App { method = 'update'; } // add auth token to header if auth is enabled - if (app.version && app.version.get('auth_required')) { - return app.keystoneClient.authenticate() - .fail(() => app.logout()) - .then(() => { - app.user.set('token', app.keystoneClient.token); - options.headers = options.headers || {}; - options.headers['X-Auth-Token'] = app.keystoneClient.token; - return originalSyncMethod.call(this, method, model, options); - }) + if (app.version.get('auth_required')) { + options.headers = options.headers || {}; + options.headers['X-Auth-Token'] = app.user.get('token'); + return originalSyncMethod.call(this, method, model, options) .fail((response) => { if (response && response.status === 401) { app.logout(); diff --git a/static/consts.js b/static/consts.js index 2e314182d..20a7ba972 100644 --- a/static/consts.js +++ b/static/consts.js @@ -91,3 +91,8 @@ export var DEPLOYMENT_GRAPH_LEVELS = [ 'plugin', 'cluster' ]; + +export var DEFAULT_ADMIN_PASSWORD = 'admin'; +export var FUEL_PROJECT_NAME = 'admin'; +export var FUEL_PROJECT_DOMAIN_NAME = 'fuel'; +export var FUEL_USER_DOMAIN_NAME = 'fuel'; diff --git a/static/keystone_client.js b/static/keystone_client.js index fb3ce3e85..d5f50871e 100644 --- a/static/keystone_client.js +++ b/static/keystone_client.js @@ -17,102 +17,98 @@ import $ from 'jquery'; import _ from 'underscore'; class KeystoneClient { - constructor(url, options) { - this.DEFAULT_PASSWORD = 'admin'; - _.extend(this, { - url: url, - cacheTokenFor: 10 * 60 * 1000 - }, options); + constructor(url) { + this.url = url; } - authenticate(username, password, options = {}) { - if (this.tokenUpdateRequest) return this.tokenUpdateRequest; + request(url, options = {}) { + return $.ajax( + this.url + url, + _.extend({}, { + dataType: 'json', + contentType: 'application/json' + }, options) + ); + } - if ( - !options.force && - this.tokenUpdateTime && - (this.cacheTokenFor > (new Date() - this.tokenUpdateTime)) - ) { - return $.Deferred().resolve(); - } - var data = {auth: {}}; - if (username && password) { - data.auth.passwordCredentials = { - username: username, - password: password - }; - } else if (this.token) { - data.auth.token = {id: this.token}; - } else { - return $.Deferred().reject(); - } - if (this.tenant) { - data.auth.tenantName = this.tenant; - } - this.tokenUpdateRequest = $.ajax(this.url + '/v2.0/tokens', { - type: 'POST', - dataType: 'json', - contentType: 'application/json', - data: JSON.stringify(data) - }).then((result, state, deferred) => { - try { - this.userId = result.access.user.id; - this.token = result.access.token.id; - this.tokenUpdateTime = new Date(); - return deferred; - } catch (e) { - return $.Deferred().reject(); + authenticate({username, password, projectName, userDomainName, projectDomainName}) { + if (this.tokenIssueRequest) return this.tokenIssueRequest; + + if (!(username && password)) return $.Deferred().reject(); + + var data = { + auth: { + identity: { + methods: ['password'], + password: { + user: { + name: username, + password: password, + domain: {name: userDomainName} + } + } + } } - }) - .fail(() => delete this.tokenUpdateTime) - .always(() => delete this.tokenUpdateRequest); + }; + if (projectName) { + data.auth.scope = { + project: { + name: projectName, + domain: {name: projectDomainName} + } + }; + } - return this.tokenUpdateRequest; + this.tokenIssueRequest = this.request('/v3/auth/tokens', { + type: 'POST', + data: JSON.stringify(data) + }) + .then((response, status, xhr) => xhr.getResponseHeader('x-subject-token')) + .always(() => delete this.tokenIssueRequest); + + return this.tokenIssueRequest; } - changePassword(currentPassword, newPassword) { + getTokenInfo(token) { + return this.request('/v3/auth/tokens', { + type: 'GET', + headers: { + 'X-Subject-Token': token, + 'X-Auth-Token': token + } + }); + } + + changePassword(token, userId, currentPassword, newPassword) { var data = { user: { password: newPassword, original_password: currentPassword } }; - return $.ajax(this.url + '/v2.0/OS-KSCRUD/users/' + this.userId, { - type: 'PATCH', - dataType: 'json', - contentType: 'application/json', + + return this.request('/v3/users/' + userId + '/password', { + type: 'POST', data: JSON.stringify(data), - headers: {'X-Auth-Token': this.token} - }).then((result, state, deferred) => { - try { - this.token = result.access.token.id; - this.tokenUpdateTime = new Date(); - return deferred; - } catch (e) { - return $.Deferred().reject(); - } - }); - } - - deauthenticate() { - var token = this.token; - - if (this.tokenUpdateRequest) return this.tokenUpdateRequest; - if (!token) return $.Deferred().reject(); - - delete this.userId; - delete this.token; - delete this.tokenUpdateTime; - - this.tokenRemoveRequest = $.ajax(this.url + '/v2.0/tokens/' + token, { - type: 'DELETE', - dataType: 'json', - contentType: 'application/json', headers: {'X-Auth-Token': token} }) - .always(() => delete this.tokenRemoveRequest); + .then((response, status, xhr) => xhr.getResponseHeader('x-subject-token')); + } - return this.tokenRemoveRequest; + deauthenticate(token) { + if (this.tokenRevokeRequest) return this.tokenRevokeRequest; + if (!token) return $.Deferred().reject(); + + this.tokenRevokeRequest = this.request('/v3/auth/tokens', { + type: 'DELETE', + headers: { + 'X-Auth-Token': token, + 'X-Subject-Token': token + } + }) + .always(() => delete this.tokenRevokeRequest); + + return this.tokenRevokeRequest; } } diff --git a/static/views/cluster_page_tabs/logs_tab.js b/static/views/cluster_page_tabs/logs_tab.js index b44aaf2fc..8691e1371 100644 --- a/static/views/cluster_page_tabs/logs_tab.js +++ b/static/views/cluster_page_tabs/logs_tab.js @@ -82,7 +82,7 @@ var LogsTab = React.createClass({ dataType: 'json', data: _.extend(_.omit(this.props.selectedLogs, 'type'), data), headers: { - 'X-Auth-Token': app.keystoneClient.token + 'X-Auth-Token': app.user.get('token') } }); }, diff --git a/static/views/controls.js b/static/views/controls.js index 8805e3f88..2fee83bf3 100644 --- a/static/views/controls.js +++ b/static/views/controls.js @@ -754,7 +754,7 @@ export var DownloadFileButton = React.createClass({ url, data: fetchOptions, dataType: 'text', - headers: _.extend({'X-Auth-Token': app.keystoneClient.token}, headers) + headers: _.extend({'X-Auth-Token': app.user.get('token')}, headers) }) .then( (response) => this.saveFile(response), diff --git a/static/views/dialogs.js b/static/views/dialogs.js index e5565a562..2cd1a655c 100644 --- a/static/views/dialogs.js +++ b/static/views/dialogs.js @@ -20,7 +20,12 @@ import i18n from 'i18n'; import React from 'react'; import ReactDOM from 'react-dom'; import Backbone from 'backbone'; -import {NODE_LIST_SORTERS, NODE_LIST_FILTERS, DEPLOYMENT_TASK_ATTRIBUTES} from 'consts'; +import { + NODE_LIST_SORTERS, NODE_LIST_FILTERS, + DEPLOYMENT_TASK_ATTRIBUTES, + DEFAULT_ADMIN_PASSWORD, + FUEL_PROJECT_NAME, FUEL_PROJECT_DOMAIN_NAME, FUEL_USER_DOMAIN_NAME +} from 'consts'; import utils from 'utils'; import models from 'models'; import dispatcher from 'dispatcher'; @@ -2227,13 +2232,29 @@ export var ChangePasswordDialog = React.createClass({ changePassword() { if (this.isPasswordChangeAvailable()) { var keystoneClient = app.keystoneClient; + var {currentPassword, newPassword} = this.state; this.setState({actionInProgress: true}); - keystoneClient.changePassword(this.state.currentPassword, this.state.newPassword) + keystoneClient.changePassword( + app.user.get('token'), + app.user.get('id'), + currentPassword, + newPassword + ) .done(() => { - dispatcher.trigger(this.state.newPassword === keystoneClient.DEFAULT_PASSWORD ? - 'showDefaultPasswordWarning' : 'hideDefaultPasswordWarning'); - app.user.set({token: keystoneClient.token}); + dispatcher.trigger( + this.state.newPassword === DEFAULT_ADMIN_PASSWORD ? + 'showDefaultPasswordWarning' : 'hideDefaultPasswordWarning' + ); this.close(); + keystoneClient.authenticate({ + username: app.user.get('username'), + password: newPassword, + projectName: FUEL_PROJECT_NAME, + userDomainName: FUEL_USER_DOMAIN_NAME, + projectDomainName: FUEL_PROJECT_DOMAIN_NAME + }).then((token) => { + app.user.set({token}); + }); }) .fail(() => { this.setState({validationError: true, actionInProgress: false}); diff --git a/static/views/login_page.js b/static/views/login_page.js index 4e72a8b3d..fb052dd7d 100644 --- a/static/views/login_page.js +++ b/static/views/login_page.js @@ -20,6 +20,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import utils from 'utils'; import dispatcher from 'dispatcher'; +import { + DEFAULT_ADMIN_PASSWORD, FUEL_PROJECT_NAME, FUEL_PROJECT_DOMAIN_NAME, FUEL_USER_DOMAIN_NAME +} from 'consts'; var LoginPage = React.createClass({ statics: { @@ -48,11 +51,16 @@ var LoginForm = React.createClass({ login(username, password) { var keystoneClient = app.keystoneClient; - return keystoneClient.authenticate(username, password, {force: true}) - .fail((xhr) => { + return keystoneClient.authenticate({ + username, password, + projectName: FUEL_PROJECT_NAME, + userDomainName: FUEL_USER_DOMAIN_NAME, + projectDomainName: FUEL_PROJECT_DOMAIN_NAME + }, {force: true}) + .fail((response) => { $(ReactDOM.findDOMNode(this.refs.username)).focus(); - var status = xhr && xhr.status; + var status = response && response.status; var error = 'login_error'; if (status === 401) { error = 'credentials_error'; @@ -62,18 +70,27 @@ var LoginForm = React.createClass({ } this.setState({error: i18n('login_page.' + error)}); }) - .then(() => { + .then((token) => { app.user.set({ authenticated: true, - username: username, - token: keystoneClient.token + username, + token }); - if (password === keystoneClient.DEFAULT_PASSWORD) { + if (password === DEFAULT_ADMIN_PASSWORD) { dispatcher.trigger('showDefaultPasswordWarning'); } - return $.when(app.version.fetch({cache: true}), app.fuelSettings.fetch({cache: true})); + return $.when( + app.version.fetch({cache: true}), + app.fuelSettings.fetch({cache: true}), + keystoneClient.getTokenInfo(token).then((tokenInfo) => { + app.user.set({ + id: tokenInfo.token.user.id, + roles: tokenInfo.token.roles + }); + }) + ); }) .then(() => { var nextUrl = '';