diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js
index a5a95846fb..a2ebe58449 100644
--- a/ui/app/components/auth-form.js
+++ b/ui/app/components/auth-form.js
@@ -1,12 +1,14 @@
import Ember from 'ember';
import { supportedAuthBackends } from 'vault/helpers/supported-auth-backends';
const BACKENDS = supportedAuthBackends();
+const { computed, inject } = Ember;
export default Ember.Component.extend({
classNames: ['auth-form'],
- routing: Ember.inject.service('-routing'),
- auth: Ember.inject.service(),
- flashMessages: Ember.inject.service(),
+ routing: inject.service('-routing'),
+ auth: inject.service(),
+ flashMessages: inject.service(),
+ csp: inject.service('csp-event'),
didRender() {
// on very narrow viewports the active tab may be overflowed, so we scroll it into view here
this.$('li.is-active').get(0).scrollIntoView();
@@ -25,9 +27,21 @@ export default Ember.Component.extend({
return `auth-form/${type}`;
}),
+ hasCSPError: computed.alias('csp.connectionViolations.firstObject'),
+
+ cspErrorText: `This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`,
+
handleError(e) {
this.set('loading', false);
- this.set('error', `Authentication failed: ${e.errors.join('.')}`);
+
+ let errors = e.errors.map(error => {
+ if (error.detail) {
+ return error.detail;
+ }
+ return error;
+ });
+
+ this.set('error', `Authentication failed: ${errors.join('.')}`);
},
actions: {
diff --git a/ui/app/components/message-error.js b/ui/app/components/message-error.js
index 04e9f22947..c2f8481150 100644
--- a/ui/app/components/message-error.js
+++ b/ui/app/components/message-error.js
@@ -1,7 +1,6 @@
import Ember from 'ember';
export default Ember.Component.extend({
- tagName: '',
model: null,
errors: [],
errorMessage: null,
diff --git a/ui/app/instance-initializers/track-csp-event.js b/ui/app/instance-initializers/track-csp-event.js
new file mode 100644
index 0000000000..bc07a25e6f
--- /dev/null
+++ b/ui/app/instance-initializers/track-csp-event.js
@@ -0,0 +1,9 @@
+export function initialize(appInstance) {
+ let service = appInstance.lookup('service:csp-event');
+ service.attach();
+}
+
+export default {
+ name: 'track-csp-event',
+ initialize,
+};
diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js
index 673e9f91fb..ca4f20742d 100644
--- a/ui/app/models/cluster.js
+++ b/ui/app/models/cluster.js
@@ -11,6 +11,7 @@ export default DS.Model.extend({
nodes: hasMany('nodes', { async: false }),
name: attr('string'),
status: attr('string'),
+ standby: attr('boolean'),
needsInit: computed('nodes', 'nodes.[]', function() {
// needs init if no nodes are initialized
diff --git a/ui/app/serializers/node.js b/ui/app/serializers/node.js
index 5820313506..e75baae9ca 100644
--- a/ui/app/serializers/node.js
+++ b/ui/app/serializers/node.js
@@ -28,9 +28,6 @@ export default DS.RESTSerializer.extend(DS.EmbeddedRecordsMixin, {
},
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
- // payload looks like:
- // { "nodes": { "name": { "sealed": "true" }}}
-
const nodes = payload.nodes
? Object.keys(payload.nodes).map(name => this.nodeFromObject(name, payload))
: [Ember.assign(payload, { id: '1' })];
diff --git a/ui/app/services/csp-event.js b/ui/app/services/csp-event.js
new file mode 100644
index 0000000000..bdec7d5dcf
--- /dev/null
+++ b/ui/app/services/csp-event.js
@@ -0,0 +1,26 @@
+import Ember from 'ember';
+const { computed } = Ember;
+
+export default Ember.Service.extend({
+ init() {
+ this._super(...arguments);
+ this.handleCSP = Ember.run.bind(this, '_handleCSP');
+ },
+
+ events: [],
+
+ _handleCSP(event) {
+ this.get('events').addObject(event);
+ },
+
+ connectionViolations: computed.filterBy('events', 'violatedDirective', 'connect-src'),
+
+ attach() {
+ this.get('events').clear();
+ window.document.addEventListener('securitypolicyviolation', this.handleCSP, true);
+ },
+
+ remove() {
+ window.document.removeEventListener('securitypolicyviolation', this.handleCSP, true);
+ },
+});
diff --git a/ui/app/templates/components/auth-form.hbs b/ui/app/templates/components/auth-form.hbs
index 506a942ec7..73b35345a4 100644
--- a/ui/app/templates/components/auth-form.hbs
+++ b/ui/app/templates/components/auth-form.hbs
@@ -10,7 +10,11 @@
- {{message-error errorMessage=error}}
+ {{#if (and cluster.standby hasCSPError)}}
+ {{message-error errorMessage=cspErrorText data-test-auth-error=true}}
+ {{else}}
+ {{message-error errorMessage=error data-test-auth-error=true}}
+ {{/if}}
{{component providerComponentName onSubmit=(action 'doSubmit') }}
{{#unless (eq selectedAuthBackend.type "token")}}
diff --git a/ui/config/environment.js b/ui/config/environment.js
index 32a9cfaca6..2ada400f9d 100644
--- a/ui/config/environment.js
+++ b/ui/config/environment.js
@@ -52,6 +52,10 @@ module.exports = function(environment) {
enabled: false,
};
}
+ if (environment !== 'production') {
+ ENV.contentSecurityPolicyHeader = 'Content-Security-Policy';
+ ENV.contentSecurityPolicyMeta = true;
+ }
if (environment === 'production') {
}
diff --git a/ui/package.json b/ui/package.json
index ce73996c2f..ee8a0f32d2 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -46,6 +46,7 @@
"ember-cli": "~2.14.0",
"ember-cli-babel": "^6.3.0",
"ember-cli-clipboard": "^0.8.0",
+ "ember-cli-content-security-policy": "^1.0.0",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-eslint": "4",
"ember-cli-favicon": "1.0.0-beta.4",
diff --git a/ui/tests/integration/components/auth-form-test.js b/ui/tests/integration/components/auth-form-test.js
new file mode 100644
index 0000000000..4bba3ce301
--- /dev/null
+++ b/ui/tests/integration/components/auth-form-test.js
@@ -0,0 +1,77 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import Ember from 'ember';
+import wait from 'ember-test-helpers/wait';
+import hbs from 'htmlbars-inline-precompile';
+
+import Pretender from 'pretender';
+import { create } from 'ember-cli-page-object';
+import authForm from '../../pages/components/auth-form';
+
+const component = create(authForm);
+
+const authService = Ember.Service.extend({
+ authenticate() {
+ return Ember.$.getJSON('http://localhost:2000');
+ },
+});
+
+moduleForComponent('auth-form', 'Integration | Component | auth form', {
+ integration: true,
+ beforeEach() {
+ Ember.getOwner(this).lookup('service:csp-event').attach();
+ component.setContext(this);
+ },
+
+ afterEach() {
+ Ember.getOwner(this).lookup('service:csp-event').remove();
+ component.removeContext();
+ },
+});
+
+const CSP_ERR_TEXT = `Error This is a standby Vault node but can't communicate with the active node via request forwarding. Sign in at the active node to use the Vault UI.`;
+test('it renders error on CSP violation', function(assert) {
+ this.register('service:auth', authService);
+ this.inject.service('auth');
+ this.set('cluster', Ember.Object.create({ standby: true }));
+ this.render(hbs`{{auth-form cluster=cluster}}`);
+ assert.equal(component.errorText, '');
+ return component.login().then(() => wait()).then(() => {
+ assert.equal(component.errorText, CSP_ERR_TEXT);
+ });
+});
+
+test('it renders with vault style errors', function(assert) {
+ let server = new Pretender(function() {
+ this.get('/v1/auth/**', () => {
+ return [
+ 400,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({
+ errors: ['Not allowed'],
+ }),
+ ];
+ });
+ });
+
+ this.set('cluster', Ember.Object.create({}));
+ this.render(hbs`{{auth-form cluster=cluster}}`);
+ return component.login().then(() => {
+ assert.equal(component.errorText, 'Error Authentication failed: Not allowed');
+ server.shutdown();
+ });
+});
+
+test('it renders AdapterError style errors', function(assert) {
+ let server = new Pretender(function() {
+ this.get('/v1/auth/**', () => {
+ return [400, { 'Content-Type': 'application/json' }];
+ });
+ });
+
+ this.set('cluster', Ember.Object.create({}));
+ this.render(hbs`{{auth-form cluster=cluster}}`);
+ return component.login().then(() => {
+ assert.equal(component.errorText, 'Error Authentication failed: Bad Request');
+ server.shutdown();
+ });
+});
diff --git a/ui/tests/pages/components/auth-form.js b/ui/tests/pages/components/auth-form.js
new file mode 100644
index 0000000000..921b300efe
--- /dev/null
+++ b/ui/tests/pages/components/auth-form.js
@@ -0,0 +1,6 @@
+import { clickable, text } from 'ember-cli-page-object';
+
+export default {
+ errorText: text('[data-test-auth-error]'),
+ login: clickable('[data-test-auth-submit]'),
+};
diff --git a/ui/yarn.lock b/ui/yarn.lock
index 1b9ff64100..d7a98cc21b 100644
--- a/ui/yarn.lock
+++ b/ui/yarn.lock
@@ -1105,6 +1105,21 @@ bmp-js@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/bmp-js/-/bmp-js-0.0.3.tgz#64113e9c7cf1202b376ed607bf30626ebe57b18a"
+body-parser@^1.17.0:
+ version "1.18.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
+ dependencies:
+ bytes "3.0.0"
+ content-type "~1.0.4"
+ debug "2.6.9"
+ depd "~1.1.1"
+ http-errors "~1.6.2"
+ iconv-lite "0.4.19"
+ on-finished "~2.3.0"
+ qs "6.5.1"
+ raw-body "2.3.2"
+ type-is "~1.6.15"
+
body@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069"
@@ -1681,6 +1696,10 @@ bytes@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a"
+bytes@3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
+
calculate-cache-key-for-tree@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/calculate-cache-key-for-tree/-/calculate-cache-key-for-tree-1.1.0.tgz#0c3e42c9c134f3c9de5358c0f16793627ea976d6"
@@ -2091,6 +2110,10 @@ content-type@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed"
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+
continuable-cache@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f"
@@ -2243,6 +2266,12 @@ debug@2.6.8, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.4.
dependencies:
ms "2.0.0"
+debug@2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
@@ -2302,6 +2331,14 @@ depd@1.1.0, depd@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
+depd@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+
+depd@~1.1.1, depd@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
@@ -2538,6 +2575,13 @@ ember-cli-clipboard@^0.8.0:
ember-cli-htmlbars "^2.0.2"
fastboot-transform "0.1.1"
+ember-cli-content-security-policy@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/ember-cli-content-security-policy/-/ember-cli-content-security-policy-1.0.0.tgz#4f7d72997d4209cd59f10d3b0070fdb39593ed2d"
+ dependencies:
+ body-parser "^1.17.0"
+ chalk "^2.0.0"
+
ember-cli-dependency-checker@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ember-cli-dependency-checker/-/ember-cli-dependency-checker-1.4.0.tgz#2b13f977e1eea843fc1a21a001be6ca5d4ef1942"
@@ -4334,6 +4378,15 @@ htmlparser2@~3.8.1:
entities "1.0"
readable-stream "1.1"
+http-errors@1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+ dependencies:
+ depd "1.1.1"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
http-errors@~1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
@@ -4343,6 +4396,15 @@ http-errors@~1.6.1:
setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2"
+http-errors@~1.6.2:
+ version "1.6.3"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+ dependencies:
+ depd "~1.1.2"
+ inherits "2.0.3"
+ setprototypeof "1.1.0"
+ statuses ">= 1.4.0 < 2"
+
http-proxy@^1.13.1, http-proxy@^1.9.0:
version "1.16.2"
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742"
@@ -4358,6 +4420,10 @@ http-signature@~1.1.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
+iconv-lite@0.4.19:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
iconv-lite@^0.4.17, iconv-lite@^0.4.5, iconv-lite@~0.4.13:
version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
@@ -6121,6 +6187,10 @@ qs@6.4.0, qs@^6.4.0, qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+qs@6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
qs@~6.3.0:
version "6.3.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
@@ -6164,6 +6234,15 @@ range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
+raw-body@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
+ dependencies:
+ bytes "3.0.0"
+ http-errors "1.6.2"
+ iconv-lite "0.4.19"
+ unpipe "1.0.0"
+
raw-body@~1.1.0:
version "1.1.7"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425"
@@ -6696,6 +6775,10 @@ setprototypeof@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
+setprototypeof@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -6927,6 +7010,10 @@ stack-trace@0.0.x:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
+"statuses@>= 1.4.0 < 2":
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
+
stdout-stream@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
@@ -7371,7 +7458,7 @@ universalify@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.0.tgz#9eb1c4651debcc670cc94f1a75762332bb967778"
-unpipe@~1.0.0:
+unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"