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"