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
Depends-On: If201c247210131ce6ab192362eada250a4f51ce1
Change-Id: I48b73a09cad0d707c16df5ca8ada202173779129
This commit is contained in:
Vitaly Kramskikh 2016-09-28 16:53:17 +03:00
parent c47f218f38
commit 2756e6b503
7 changed files with 166 additions and 117 deletions

View File

@ -78,7 +78,7 @@ class Router extends Backbone.Router {
}},
{name: 'welcome', condition: (previousUrl) => {
return previousUrl !== 'logout' &&
_.find(app.keystoneClient.userRoles, {name: 'admin'}) &&
_.find(app.user.get('roles'), {name: 'admin'}) &&
!app.fuelSettings.get('statistics.user_choice_saved.value');
}}
];
@ -173,11 +173,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() {
@ -186,27 +182,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()
.catch((response) => {
if (response.status === 401) {
this.version.set({auth_required: true});
if (response.status !== 401) {
isNailgunAvailable = false;
}
return Promise.reject(response);
})
.then(() => {
this.user.set({authenticated: !this.version.get('auth_required')});
this.user.set({authenticated: true});
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 this.keystoneClient.getTokenInfo(this.user.get('token'))
.then((tokenInfo) => {
this.user.set({
id: tokenInfo.token.user.id,
roles: tokenInfo.token.roles
});
})
.catch(() => {
this.user.set({authenticated: false});
this.user.unset('token');
this.user.unset('username');
return Promise.reject();
});
} else {
return Promise.resolve();
}
return Promise.resolve();
})
.then(() => this.fuelSettings.fetch())
.catch(() => {
if (this.version.get('auth_required') && !this.user.get('authenticated')) {
if (isNailgunAvailable) {
return Promise.resolve();
} else {
this.mountNode.empty();
@ -250,11 +260,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}));
@ -269,18 +283,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()
.catch(() => {
app.logout();
return Promise.reject();
})
.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)
.catch((response) => {
if (response && response.status === 401) {
app.logout();

View File

@ -91,3 +91,8 @@ export const DEPLOYMENT_GRAPH_LEVELS = [
'plugin',
'cluster'
];
export const DEFAULT_ADMIN_PASSWORD = 'admin';
export const FUEL_PROJECT_NAME = 'admin';
export const FUEL_PROJECT_DOMAIN_NAME = 'fuel';
export const FUEL_USER_DOMAIN_NAME = 'fuel';

View File

@ -16,105 +16,107 @@
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;
}
request(url, options = {}) {
options.headers = new Headers(_.extend({}, {
'Content-Type': 'application/json'
}, options.headers));
return fetch(this.url + url, options)
.then((response) => response.json());
return fetch(this.url + url, options).then((response) => {
if (!response.ok) throw response;
return response;
});
}
authenticate(username, password, options = {}) {
if (this.tokenUpdatePromise) return this.tokenUpdatePromise;
authenticate({username, password, projectName, userDomainName, projectDomainName}) {
if (this.tokenIssueRequest) return this.tokenIssueRequest;
if (
!options.force &&
this.tokenUpdateTime &&
(this.cacheTokenFor > (new Date() - this.tokenUpdateTime))
) {
return Promise.resolve();
}
var data = {auth: {}};
if (username && password) {
data.auth.passwordCredentials = {
username: username,
password: password
if (!(username && password)) return Promise.reject();
var data = {
auth: {
identity: {
methods: ['password'],
password: {
user: {
name: username,
password: password,
domain: {name: userDomainName}
}
}
}
}
};
if (projectName) {
data.auth.scope = {
project: {
name: projectName,
domain: {name: projectDomainName}
}
};
} else if (this.token) {
data.auth.token = {id: this.token};
} else {
return Promise.reject();
}
if (this.tenant) {
data.auth.tenantName = this.tenant;
}
this.tokenUpdatePromise = this.request('/v2.0/tokens', {
this.tokenIssueRequest = this.request('/v3/auth/tokens', {
method: 'POST',
body: JSON.stringify(data)
}).then((result) => {
this.userId = result.access.user.id;
this.userRoles = result.access.user.roles;
this.token = result.access.token.id;
this.tokenUpdateTime = new Date();
}).then((response) => {
return response.headers.get('X-Subject-Token');
});
this.tokenUpdatePromise
.catch(() => delete this.tokenUpdateTime)
.then(() => delete this.tokenUpdatePromise);
this.tokenIssueRequest
.catch(() => true)
.then(() => delete this.tokenIssueRequest);
return this.tokenUpdatePromise;
return this.tokenIssueRequest;
}
changePassword(currentPassword, newPassword) {
getTokenInfo(token) {
return this.request('/v3/auth/tokens', {
method: 'GET',
headers: {
'X-Subject-Token': token,
'X-Auth-Token': token
}
}).then((response) => response.json());
}
changePassword(token, userId, currentPassword, newPassword) {
var data = {
user: {
password: newPassword,
original_password: currentPassword
}
};
return this.request('/v2.0/OS-KSCRUD/users/' + this.userId, {
method: 'PATCH',
return this.request('/v3/users/' + userId + '/password', {
method: 'POST',
headers: {
'X-Auth-Token': this.token
'X-Auth-Token': token
},
body: JSON.stringify(data)
}).then((result) => {
this.token = result.access.token.id;
this.tokenUpdateTime = new Date();
}).then((response) => {
return response.headers.get('X-Subject-Token');
});
}
deauthenticate() {
var token = this.token;
if (this.tokenUpdatePromise) return this.tokenUpdatePromise;
deauthenticate(token) {
if (this.tokenRevokeRequest) return this.tokenRevokeRequest;
if (!token) return Promise.reject();
delete this.userId;
delete this.userRoles;
delete this.token;
delete this.tokenUpdateTime;
this.tokenRemoveRequest = this.request('/v2.0/tokens/' + token, {
this.tokenRevokeRequest = this.request('/v3/auth/tokens', {
method: 'DELETE',
headers: {
'X-Auth-Token': this.token
'X-Auth-Token': token,
'X-Subject-Token': token
}
});
this.tokenRemoveRequest
this.tokenRevokeRequest
.catch(() => true)
.then(() => delete this.tokenRemoveRequest);
.then(() => delete this.tokenRevokeRequest);
return this.tokenRemoveRequest;
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

@ -741,7 +741,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';
@ -2190,20 +2195,35 @@ 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)
.then(
() => {
dispatcher.trigger(this.state.newPassword === keystoneClient.DEFAULT_PASSWORD ?
'showDefaultPasswordWarning' : 'hideDefaultPasswordWarning');
app.user.set({token: keystoneClient.token});
this.close();
},
() => {
this.setState({validationError: true, actionInProgress: false});
$(this.refs.currentPassword.getInputDOMNode()).focus();
}
);
keystoneClient.changePassword(
app.user.get('token'),
app.user.get('id'),
currentPassword,
newPassword
).then(
() => {
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});
});
},
() => {
this.setState({validationError: true, actionInProgress: false});
$(this.refs.currentPassword.getInputDOMNode()).focus();
}
);
}
}
});

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})
.catch((xhr) => {
return keystoneClient.authenticate({
username, password,
projectName: FUEL_PROJECT_NAME,
userDomainName: FUEL_USER_DOMAIN_NAME,
projectDomainName: FUEL_PROJECT_DOMAIN_NAME
}, {force: true})
.catch((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';
@ -64,19 +72,27 @@ var LoginForm = React.createClass({
return Promise.reject();
})
.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 Promise.all([app.version.fetch({cache: true}),
app.fuelSettings.fetch({cache: true})]);
return Promise.all([
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 = '/';