Require auth for version api call

The version call may contain information that should not be publicly
discloused without authentication. This change removes the version call
from the public urls which will cause it to require authentication
before responding with version information.

(cherry-picked from 5372982b3e)
(cherry-picked from 71738018b0)

Note on the backport: Some tests of a public API were not present in the
original commit. They are moved to the newly created version API tests.
Also partially included the change from fuel-ui which is part of nailgun
in 7.0.

Change-Id: I7352f83ec67321a1fb5caa75fc9168cc01229083
Closes-Bug: #1585137
This commit is contained in:
Alex Schultz 2016-05-24 10:55:36 -06:00 committed by Denis V. Meltsaykin
parent fb491402d9
commit 281bab6586
8 changed files with 114 additions and 65 deletions

View File

@ -366,5 +366,4 @@ def public_urls():
return { return {
r'/nodes/?$': ['POST'], r'/nodes/?$': ['POST'],
r'/nodes/agent/?$': ['PUT'], r'/nodes/agent/?$': ['PUT'],
r'/version/?$': ['GET']
} }

View File

@ -14,7 +14,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from mock import patch
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from nailgun.test.base import BaseAuthenticationIntegrationTest from nailgun.test.base import BaseAuthenticationIntegrationTest
@ -47,37 +46,3 @@ class TestPublicHandlers(BaseAuthenticationIntegrationTest):
headers=self.default_headers) headers=self.default_headers)
self.assertEqual(201, resp.status_code) self.assertEqual(201, resp.status_code)
def test_version_api(self):
resp = self.app.get(
reverse('VersionHandler'),
headers=self.default_headers
)
self.assertEqual(200, resp.status_code)
@patch('nailgun.api.v1.handlers.version.utils.get_fuel_release_versions')
def test_500_no_html_dev(self, handler_get):
exc_text = "Here goes an exception"
handler_get.side_effect = Exception(exc_text)
resp = self.app.get(
reverse('VersionHandler'),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(500, resp.status_code)
self.assertIn(exc_text, resp.body)
self.assertIn("Traceback", resp.body)
self.assertNotIn("html", resp.body)
@patch('nailgun.api.v1.handlers.version.utils.get_fuel_release_versions')
def test_500_no_html_production(self, handler_get):
exc_text = "Here goes an exception"
handler_get.side_effect = Exception(exc_text)
with patch('nailgun.settings.settings.DEVELOPMENT', 0):
resp = self.app.get(
reverse('VersionHandler'),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(500, resp.status_code)
self.assertEqual(exc_text, resp.body)

View File

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from mock import patch
from nailgun.test.base import BaseAuthenticationIntegrationTest
from nailgun.test.base import reverse
class TestVersionApi(BaseAuthenticationIntegrationTest):
"""Test the version api
Test the version api to make sure it requires authentication
and works when passed a valid auth token.
"""
def setUp(self):
super(TestVersionApi, self).setUp()
self.token = self.get_auth_token()
self.headers = copy.deepcopy(self.default_headers)
def test_version_api_noauth(self):
"""Check that version api requires auth."""
resp = self.app.get(
reverse('VersionHandler'),
headers=self.default_headers,
expect_errors=True
)
self.assertEqual(401, resp.status_code)
def test_version_api_auth(self):
"""Check that version api works with auth."""
self.headers['X-Auth-Token'] = self.token
resp = self.app.get(
reverse('VersionHandler'),
headers=self.headers
)
self.assertEqual(200, resp.status_code)
@patch('nailgun.api.v1.handlers.version.utils.get_fuel_release_versions')
def test_500_no_html_dev(self, handler_get):
exc_text = "Here goes an exception"
handler_get.side_effect = Exception(exc_text)
self.headers['X-Auth-Token'] = self.token
resp = self.app.get(
reverse('VersionHandler'),
headers=self.headers,
expect_errors=True
)
self.assertEqual(500, resp.status_code)
self.assertIn(exc_text, resp.body)
self.assertIn("Traceback", resp.body)
self.assertNotIn("html", resp.body)
@patch('nailgun.api.v1.handlers.version.utils.get_fuel_release_versions')
def test_500_no_html_production(self, handler_get):
exc_text = "Here goes an exception"
handler_get.side_effect = Exception(exc_text)
self.headers['X-Auth-Token'] = self.token
with patch('nailgun.settings.settings.DEVELOPMENT', 0):
resp = self.app.get(
reverse('VersionHandler'),
headers=self.headers,
expect_errors=True
)
self.assertEqual(500, resp.status_code)
self.assertEqual(exc_text, resp.body)

View File

@ -153,29 +153,35 @@ function($, _, i18n, Backbone, React, utils, layoutComponents, Coccyx, models, K
this.mountNode = $('#main-container'); this.mountNode = $('#main-container');
this.router = new Router(); this.router = new Router();
this.keystoneClient = new KeystoneClient('/keystone', {
cacheTokenFor: 10 * 60 * 1000,
tenant: 'admin'
});
this.version = new models.FuelVersion(); this.version = new models.FuelVersion();
this.settings = new models.FuelSettings(); this.settings = new models.FuelSettings();
this.user = new models.User(); this.user = new models.User();
this.statistics = new models.NodesStatistics(); this.statistics = new models.NodesStatistics();
this.notifications = new models.Notifications(); this.notifications = new models.Notifications();
this.keystoneClient = new KeystoneClient('/keystone', {
cacheTokenFor: 10 * 60 * 1000,
tenant: 'admin',
token: this.user.get('token')
});
this.fetchData(); this.fetchData();
} }
_.extend(App.prototype, { _.extend(App.prototype, {
fetchData: function() { fetchData: function() {
this.version.fetch().then(_.bind(function() { this.version.fetch().then(null, _.bind(function(response) {
if (response.status == 401) {
this.version.set({auth_required: true});
return $.Deferred().resolve();
}
}, this)).then(_.bind(function() {
this.user.set({authenticated: !this.version.get('auth_required')}); this.user.set({authenticated: !this.version.get('auth_required')});
this.patchBackboneSync(); this.patchBackboneSync();
if (this.version.get('auth_required')) { if (this.version.get('auth_required')) {
_.extend(this.keystoneClient, this.user.pick('token')); _.extend(this.keystoneClient, this.user.pick('token'));
return this.keystoneClient.authenticate() return this.keystoneClient.authenticate()
.done(_.bind(function() { .then(_.bind(function() {
this.user.set({authenticated: true}); this.user.set({authenticated: true});
return this.version.fetch({cache: true});
}, this)); }, this));
} }
return $.Deferred().resolve(); return $.Deferred().resolve();
@ -233,7 +239,7 @@ function($, _, i18n, Backbone, React, utils, layoutComponents, Coccyx, models, K
if (method == 'patch') { if (method == 'patch') {
method = 'update'; method = 'update';
} }
if (app.version.get('auth_required') && !this.authExempt) { if (app.version && app.version.get('auth_required')) {
// FIXME(vkramskikh): manually moving success/error callbacks // FIXME(vkramskikh): manually moving success/error callbacks
// to deferred-style callbacks. Everywhere in the code we use // to deferred-style callbacks. Everywhere in the code we use
// deferreds, but backbone uses success/error callbacks. It // deferreds, but backbone uses success/error callbacks. It
@ -249,6 +255,7 @@ function($, _, i18n, Backbone, React, utils, layoutComponents, Coccyx, models, K
app.logout(); app.logout();
}) })
.then(_.bind(function() { .then(_.bind(function() {
app.user.set('token', app.keystoneClient.token);
options = options || {}; options = options || {};
options.headers = options.headers || {}; options.headers = options.headers || {};
options.headers['X-Auth-Token'] = app.keystoneClient.token; options.headers['X-Auth-Token'] = app.keystoneClient.token;

View File

@ -13,7 +13,7 @@
* License for the specific language governing permissions and limitations * License for the specific language governing permissions and limitations
* under the License. * under the License.
**/ **/
define(['jquery', 'underscore', 'js-cookie'], function($, _, Cookies) { define(['jquery', 'underscore'], function($, _) {
'use strict'; 'use strict';
function KeystoneClient(url, options) { function KeystoneClient(url, options) {
@ -57,9 +57,6 @@ define(['jquery', 'underscore', 'js-cookie'], function($, _, Cookies) {
this.userId = result.access.user.id; this.userId = result.access.user.id;
this.token = result.access.token.id; this.token = result.access.token.id;
this.tokenUpdateTime = new Date(); this.tokenUpdateTime = new Date();
Cookies.set('token', result.access.token.id);
return deferred; return deferred;
} catch(e) { } catch(e) {
return $.Deferred().reject(); return $.Deferred().reject();
@ -88,9 +85,6 @@ define(['jquery', 'underscore', 'js-cookie'], function($, _, Cookies) {
try { try {
this.token = result.access.token.id; this.token = result.access.token.id;
this.tokenUpdateTime = new Date(); this.tokenUpdateTime = new Date();
Cookies.set('token', result.access.token.id);
return deferred; return deferred;
} catch(e) { } catch(e) {
return $.Deferred().reject(); return $.Deferred().reject();
@ -111,8 +105,6 @@ define(['jquery', 'underscore', 'js-cookie'], function($, _, Cookies) {
delete this.token; delete this.token;
delete this.tokenUpdateTime; delete this.tokenUpdateTime;
Cookies.remove('token');
this.tokenRemoveRequest = $.ajax(this.url + '/v2.0/tokens/' + token, { this.tokenRemoveRequest = $.ajax(this.url + '/v2.0/tokens/' + token, {
type: 'DELETE', type: 'DELETE',
dataType: 'json', dataType: 'json',

View File

@ -22,8 +22,9 @@ define([
'expression', 'expression',
'expression/objects', 'expression/objects',
'jsx!views/custom_controls', 'jsx!views/custom_controls',
'js-cookie',
'deepModel' 'deepModel'
], function($, _, i18n, Backbone, utils, Expression, expressionObjects, customControls) { ], function($, _, i18n, Backbone, utils, Expression, expressionObjects, customControls, Cookies) {
'use strict'; 'use strict';
var models = {}; var models = {};
@ -83,6 +84,7 @@ define([
if (this.cacheFor && options && options.cache && this.lastSyncTime && (this.cacheFor > (new Date() - this.lastSyncTime))) { if (this.cacheFor && options && options.cache && this.lastSyncTime && (this.cacheFor > (new Date() - this.lastSyncTime))) {
return $.Deferred().resolve(); return $.Deferred().resolve();
} }
if (options) delete options.cache;
return this._super('fetch', arguments); return this._super('fetch', arguments);
}, },
sync: function() { sync: function() {
@ -998,10 +1000,10 @@ define([
urlRoot: '/api/ostf' urlRoot: '/api/ostf'
}); });
models.FuelVersion = BaseModel.extend({ models.FuelVersion = BaseModel.extend(cacheMixin).extend({
cacheFor: 60 * 1000,
constructorName: 'FuelVersion', constructorName: 'FuelVersion',
urlRoot: '/api/version', urlRoot: '/api/version'
authExempt: true
}); });
models.User = BaseModel.extend({ models.User = BaseModel.extend({
@ -1021,6 +1023,14 @@ define([
} }
}); });
}, this); }, this);
this.on('change:token', function() {
var token = this.get('token');
if (_.isUndefined(token)) {
Cookies.remove('token');
} else {
Cookies.set('token', token);
}
}, this);
} }
}); });

View File

@ -40,12 +40,6 @@ function($, _, i18n, React, dispatcher, utils) {
<LoginForm /> <LoginForm />
</div> </div>
</div> </div>
<div className='login-footer col-xs-12'>
{_.contains(app.version.get('feature_groups'), 'mirantis') &&
<p className='text-center'>{i18n('common.copyright')}</p>
}
<p className='text-center'>{i18n('common.version')}: {app.version.get('release')}</p>
</div>
</div> </div>
); );
} }
@ -71,7 +65,7 @@ function($, _, i18n, React, dispatcher, utils) {
dispatcher.trigger('showDefaultPasswordWarning'); dispatcher.trigger('showDefaultPasswordWarning');
} }
return app.settings.fetch({cache: true}); return $.when(app.version.fetch({cache: true}), app.settings.fetch({cache: true}));
}, this)) }, this))
.done(_.bind(function() { .done(_.bind(function() {
var nextUrl = ''; var nextUrl = '';

View File

@ -630,7 +630,7 @@ function run_server() {
local http_code=$(curl -s -w %{http_code} -o /dev/null $check_url) local http_code=$(curl -s -w %{http_code} -o /dev/null $check_url)
if [[ "$http_code" = "200" ]]; then return 0; fi if [[ "$http_code" != "000" ]]; then return 0; fi
sleep 0.1 sleep 0.1
i=$((i + 1)) i=$((i + 1))