Merge pull request #3377 from hashicorp/b-ui-gracefully-handle-403s

Gracefully handle 403s in the UI
This commit is contained in:
Michael Lange 2017-10-16 13:26:59 -07:00 committed by GitHub
commit c04d9020c2
8 changed files with 70 additions and 18 deletions

View File

@ -1,5 +1,6 @@
import Ember from 'ember'; import Ember from 'ember';
import RESTAdapter from 'ember-data/adapters/rest'; import RESTAdapter from 'ember-data/adapters/rest';
import codesForError from '../utils/codes-for-error';
const { get, computed, inject } = Ember; const { get, computed, inject } = Ember;
@ -21,8 +22,12 @@ export default RESTAdapter.extend({
findAll() { findAll() {
return this._super(...arguments).catch(error => { return this._super(...arguments).catch(error => {
if (error.code === '501' || (error.errors && error.errors.findBy('status', '501'))) { const errorCodes = codesForError(error);
// Feature is not implemented in this version of Nomad
const isNotAuthorized = errorCodes.includes('403');
const isNotImplemented = errorCodes.includes('501');
if (isNotAuthorized || isNotImplemented) {
return []; return [];
} }

View File

@ -1,4 +1,5 @@
import Ember from 'ember'; import Ember from 'ember';
import codesForError from '../utils/codes-for-error';
const { Controller, computed, inject, run, observer } = Ember; const { Controller, computed, inject, run, observer } = Ember;
@ -12,19 +13,11 @@ export default Controller.extend({
}), }),
errorCodes: computed('error', function() { errorCodes: computed('error', function() {
const error = this.get('error'); return codesForError(this.get('error'));
const codes = [error.code]; }),
if (error.errors) { is403: computed('errorCodes.[]', function() {
error.errors.forEach(err => { return this.get('errorCodes').includes('403');
codes.push(err.status);
});
}
return codes
.compact()
.uniq()
.map(code => '' + code);
}), }),
is404: computed('errorCodes.[]', function() { is404: computed('errorCodes.[]', function() {

View File

@ -11,10 +11,13 @@ export default Route.extend({
actions: { actions: {
didTransition() { didTransition() {
this.controllerFor('application').set('error', null);
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, },
willTransition() {
this.controllerFor('application').set('error', null);
},
error(error) { error(error) {
this.controllerFor('application').set('error', error); this.controllerFor('application').set('error', error);
}, },

View File

@ -8,7 +8,7 @@ export default Route.extend({
model({ job_id }) { model({ job_id }) {
return this.get('store') return this.get('store')
.find('job', job_id) .findRecord('job', job_id, { reload: true })
.then(job => { .then(job => {
return job.get('allocations').then(() => job); return job.get('allocations').then(() => job);
}) })

View File

@ -28,7 +28,7 @@ export default ApplicationSerializer.extend({
}, },
normalizeResponse(store, typeClass, hash, ...args) { normalizeResponse(store, typeClass, hash, ...args) {
return this._super(store, typeClass, hash.Members, ...args); return this._super(store, typeClass, hash.Members || [], ...args);
}, },
normalizeSingleResponse(store, typeClass, hash, id, ...args) { normalizeSingleResponse(store, typeClass, hash, id, ...args) {

View File

@ -10,6 +10,13 @@
{{else if is404}} {{else if is404}}
<h1 class="title is-spaced">Not Found</h1> <h1 class="title is-spaced">Not Found</h1>
<p class="subtitle">What you're looking for couldn't be found. It either doesn't exist or you are not authorized to see it.</p> <p class="subtitle">What you're looking for couldn't be found. It either doesn't exist or you are not authorized to see it.</p>
{{else if is403}}
<h1 class="title is-spaced">Not Authorized</h1>
{{#if token.secret}}
<p class="subtitle">Your {{#link-to "settings.tokens"}}ACL token{{/link-to}} does not provide the required permissions. Contact your administrator if this is an error.</p>
{{else}}
<p class="subtitle">Provide an {{#link-to "settings.tokens"}}ACL token{{/link-to}} with requisite permissions to view this.</p>
{{/if}}
{{else}} {{else}}
<h1 class="title is-spaced">Error</h1> <h1 class="title is-spaced">Error</h1>
<p class="subtitle">Something went wrong.</p> <p class="subtitle">Something went wrong.</p>

View File

@ -0,0 +1,15 @@
// Returns an array of error codes as strings for an Ember error object
export default function codesForError(error) {
const codes = [error.code];
if (error.errors) {
error.errors.forEach(err => {
codes.push(err.status);
});
}
return codes
.compact()
.uniq()
.map(code => '' + code);
}

View File

@ -11,7 +11,7 @@ moduleForAcceptance('Acceptance | application errors ', {
}); });
test('transitioning away from an error page resets the global error', function(assert) { test('transitioning away from an error page resets the global error', function(assert) {
server.pretender.get('/v1/nodes', () => [403, {}, null]); server.pretender.get('/v1/nodes', () => [500, {}, null]);
visit('/nodes'); visit('/nodes');
@ -25,3 +25,32 @@ test('transitioning away from an error page resets the global error', function(a
assert.notOk(find('.error-message'), 'Application is no longer in an error state'); assert.notOk(find('.error-message'), 'Application is no longer in an error state');
}); });
}); });
test('the 403 error page links to the ACL tokens page', function(assert) {
const job = server.db.jobs[0];
server.pretender.get(`/v1/job/${job.id}`, () => [403, {}, null]);
visit(`/jobs/${job.id}`);
andThen(() => {
assert.ok(find('.error-message'), 'Error message is shown');
assert.equal(
find('.error-message .title').textContent,
'Not Authorized',
'Error message is for 403'
);
});
andThen(() => {
click('.error-message a');
});
andThen(() => {
assert.equal(
currentURL(),
'/settings/tokens',
'Error message contains a link to the tokens page'
);
});
});