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
This commit is contained in:
Vitaly Kramskikh 2016-09-28 16:53:17 +03:00 committed by Julia Aranovich
parent 5586bfaad4
commit 917dd0835a
7 changed files with 174 additions and 127 deletions

View File

@ -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();

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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')
}
});
},

View File

@ -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),

View File

@ -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});

View File

@ -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 = '';