From 9026da46d8614124601b300f3d641e5f96989400 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Wed, 26 Sep 2018 21:46:00 -0500 Subject: [PATCH 1/9] allow for enterprise init attributes --- ui/app/controllers/vault/cluster/init.js | 12 +++- ui/app/templates/vault/cluster/init.hbs | 78 +++++++++++------------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/ui/app/controllers/vault/cluster/init.js b/ui/app/controllers/vault/cluster/init.js index ae6a66c2e4e9..cb329a835a89 100644 --- a/ui/app/controllers/vault/cluster/init.js +++ b/ui/app/controllers/vault/cluster/init.js @@ -41,14 +41,22 @@ export default Controller.extend(DEFAULTS, { actions: { initCluster(data) { if (data.secret_shares) { - data.secret_shares = parseInt(data.secret_shares); + let shares = parseInt(data.secret_shares, 10); + data.secret_shares = shares; + data.stored_shares = shares; + data.recovery_shares = shares; } if (data.secret_threshold) { - data.secret_threshold = parseInt(data.secret_threshold); + let threshold = parseInt(data.secret_threshold, 10); + data.secret_threshold = threshold; + data.recovery_threshold = threshold; } if (!data.use_pgp) { delete data.pgp_keys; + } else { + data.recovery_pgp_keys = data.pgp_keys; } + if (!data.use_pgp_for_root) { delete data.root_token_pgp_key; } diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs index b3be6a9a07e9..19e5691bf435 100644 --- a/ui/app/templates/vault/cluster/init.hbs +++ b/ui/app/templates/vault/cluster/init.hbs @@ -1,20 +1,32 @@ {{#if keyData}} -

- Vault has been initialized! {{#if (eq keyData.keys.length 1)}} - Here is your key. - {{else}} - Here are your {{pluralize keyData.keys.length "key"}}. - {{/if}} -

+ {{#with (or keyData.recovery_keys keyData.keys) as |keyArray|}} +

+ Vault has been initialized! {{#if (eq keyArray.length 1)}} + Here is your key. + {{else}} + Here are your {{pluralize keyArray.length "key"}}. + {{/if}} +

+ {{/with}}

- Please securely distribute the keys below. When the Vault is re-sealed, restarted, or stopped, you must provide at least {{secret_threshold}} of these keys to unseal it again. - Vault does not store the master key. Without at least {{secret_threshold}} keys, your Vault will remain permanently sealed. + {{#if keyData.recovery_keys}} + Please securely distribute the keys below. Certain privileged operations in Vault such as rekeying the + barrier or generating a new root token will require you to provide + at least {{secret_threshold}} of these keys to perform the + operation. + {{else}} + Please securely distribute the keys below. When the Vault is re-sealed, restarted, or stopped, you must + provide at least {{secret_threshold}} of these keys to unseal it + again. + Vault does not store the master key. Without at least {{secret_threshold}} + keys, your Vault will remain permanently sealed. + {{/if}}

@@ -26,7 +38,7 @@ {{keyData.root_token}}
- {{#each (if keyData.keys_base64 keyData.keys_base64 keyData.keys) as |key index| }} + {{#each (or keyData.recovery_keys_base64 keyData.recovery_keys keyData.keys_base64 keyData.keys) as |key index| }}
@@ -43,24 +55,18 @@ {{#if model.sealed}}
{{#link-to 'vault.cluster.unseal' model.name class="button is-primary"}} - Continue to Unseal + Continue to Unseal {{/link-to}}
{{else}}
{{#link-to 'vault.cluster.auth' model.name class="button is-primary"}} - Continue to Authenticate + Continue to Authenticate {{/link-to}}
{{/if}} - + Download Keys
@@ -73,7 +79,8 @@ -
+ id="init">
@@ -110,13 +116,8 @@

- + {{#if use_pgp}}

@@ -125,13 +126,8 @@

{{/if}} - + {{#if use_pgp_for_root}}

@@ -142,11 +138,7 @@ {{/if}}

- @@ -157,4 +149,4 @@ {{/if}} - + \ No newline at end of file From b88895fbb32beec46cc6211954073ecbb1dd73ff Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Wed, 26 Sep 2018 21:46:44 -0500 Subject: [PATCH 2/9] allow moving from init to auth in the init flow on the tutorial machine --- ui/app/machines/tutorial-machine.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/app/machines/tutorial-machine.js b/ui/app/machines/tutorial-machine.js index 76c32d34bc19..8ed5ca5adf55 100644 --- a/ui/app/machines/tutorial-machine.js +++ b/ui/app/machines/tutorial-machine.js @@ -34,7 +34,10 @@ export default { onEntry: { type: 'render', level: 'feature', component: 'wizard/init-setup' }, }, save: { - on: { TOUNSEAL: 'unseal' }, + on: { + TOUNSEAL: 'unseal', + TOLOGIN: 'login', + }, onEntry: { type: 'render', level: 'feature', component: 'wizard/init-save-keys' }, }, unseal: { From 133ec616fc1b105fa961120c95b5396f002e7126 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Wed, 26 Sep 2018 22:11:22 -0500 Subject: [PATCH 3/9] show loading spinner while cluster is unsealing --- ui/app/templates/vault/cluster/init.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs index 19e5691bf435..adda7162b4f6 100644 --- a/ui/app/templates/vault/cluster/init.hbs +++ b/ui/app/templates/vault/cluster/init.hbs @@ -52,7 +52,7 @@
- {{#if model.sealed}} + {{#if (and model.sealed (not keyData.recovery_keys))}}
{{#link-to 'vault.cluster.unseal' model.name class="button is-primary"}} Continue to Unseal @@ -60,7 +60,7 @@
{{else}}
- {{#link-to 'vault.cluster.auth' model.name class="button is-primary"}} + {{#link-to 'vault.cluster.auth' model.name class=(concat (if model.sealed 'is-loading ' '') 'button is-primary') disabled=model.sealed}} Continue to Authenticate {{/link-to}}
From 77cddb90d4c183d48858146a04395708175ea9a3 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Thu, 27 Sep 2018 15:05:46 -0500 Subject: [PATCH 4/9] use seal-status type to determine the init attrs --- ui/app/controllers/vault/cluster/init.js | 15 +++++++++++---- ui/app/models/cluster.js | 6 ++---- ui/app/models/node.js | 5 +---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ui/app/controllers/vault/cluster/init.js b/ui/app/controllers/vault/cluster/init.js index cb329a835a89..55425387dfa9 100644 --- a/ui/app/controllers/vault/cluster/init.js +++ b/ui/app/controllers/vault/cluster/init.js @@ -13,6 +13,7 @@ const DEFAULTS = { export default Controller.extend(DEFAULTS, { wizard: service(), + model: null, reset() { this.setProperties(DEFAULTS); @@ -40,20 +41,26 @@ export default Controller.extend(DEFAULTS, { actions: { initCluster(data) { + let isCloudSeal = this.model.sealType !== 'shamir'; if (data.secret_shares) { let shares = parseInt(data.secret_shares, 10); data.secret_shares = shares; - data.stored_shares = shares; - data.recovery_shares = shares; + if (isCloudSeal) { + data.stored_shares = shares; + data.recovery_shares = shares; + } } if (data.secret_threshold) { let threshold = parseInt(data.secret_threshold, 10); data.secret_threshold = threshold; - data.recovery_threshold = threshold; + if (isCloudSeal) { + data.recovery_threshold = threshold; + } } if (!data.use_pgp) { delete data.pgp_keys; - } else { + } + if (data.use_pgp && isCloudSeal) { data.recovery_pgp_keys = data.pgp_keys; } diff --git a/ui/app/models/cluster.js b/ui/app/models/cluster.js index e065d7483b6a..d388ce8c0b9b 100644 --- a/ui/app/models/cluster.js +++ b/ui/app/models/cluster.js @@ -12,16 +12,13 @@ export default DS.Model.extend({ name: attr('string'), status: attr('string'), standby: attr('boolean'), + type: attr('string'), needsInit: computed('nodes', 'nodes.[]', function() { // needs init if no nodes are initialized return this.get('nodes').isEvery('initialized', false); }), - type: computed(function() { - return this.constructor.modelName; - }), - unsealed: computed('nodes', 'nodes.{[],@each.sealed}', function() { // unsealed if there's at least one unsealed node return !!this.get('nodes').findBy('sealed', false); @@ -40,6 +37,7 @@ export default DS.Model.extend({ sealThreshold: alias('leaderNode.sealThreshold'), sealProgress: alias('leaderNode.progress'), + sealType: alias('leaderNode.type'), hasProgress: gte('sealProgress', 1), //replication mode - will only ever be 'unsupported' diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 29c1937b6c68..e70cbf3563c6 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -24,13 +24,10 @@ export default DS.Model.extend({ sealThreshold: alias('t'), sealNumShares: alias('n'), version: attr('string'), + type: attr('string'), //https://www.vaultproject.io/docs/http/sys-leader.html haEnabled: attr('boolean'), isSelf: attr('boolean'), leaderAddress: attr('string'), - - type: computed(function() { - return this.constructor.modelName; - }), }); From c34e3dceddb16d8abdb91a92d453854225df3995 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Thu, 27 Sep 2018 15:27:39 -0500 Subject: [PATCH 5/9] add init acceptance tests --- ui/app/templates/vault/cluster/init.hbs | 12 +-- ui/tests/acceptance/init-test.js | 117 ++++++++++++++++++++++++ ui/tests/pages/init.js | 16 ++++ 3 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 ui/tests/acceptance/init-test.js create mode 100644 ui/tests/pages/init.js diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs index adda7162b4f6..4d4f722df84b 100644 --- a/ui/app/templates/vault/cluster/init.hbs +++ b/ui/app/templates/vault/cluster/init.hbs @@ -39,7 +39,7 @@
{{#each (or keyData.recovery_keys_base64 keyData.recovery_keys keyData.keys_base64 keyData.keys) as |key index| }} -
+

@@ -53,13 +53,13 @@
{{#if (and model.sealed (not keyData.recovery_keys))}} -
+
{{#link-to 'vault.cluster.unseal' model.name class="button is-primary"}} Continue to Unseal {{/link-to}}
{{else}} -
+
{{#link-to 'vault.cluster.auth' model.name class=(concat (if model.sealed 'is-loading ' '') 'button is-primary') disabled=model.sealed}} Continue to Authenticate {{/link-to}} @@ -98,7 +98,7 @@ Key Shares
- {{input class="input" autocomplete="off" name="key-shares" type="number" step="1" min="1" pattern="[0-9]*" value=secret_shares}} + {{input data-test-key-shares="true" class="input" autocomplete="off" name="key-shares" type="number" step="1" min="1" pattern="[0-9]*" value=secret_shares}}

The number of key shares to split the master key into @@ -109,7 +109,7 @@ Key Threshold

- {{input class="input" autocomplete="off" name="key-threshold" type="number" step="1" min="1" pattern="[0-9]*" value=secret_threshold}} + {{input data-test-key-threshold="true" class="input" autocomplete="off" name="key-threshold" type="number" step="1" min="1" pattern="[0-9]*" value=secret_threshold}}

The number of key shares required to reconstruct the master key @@ -138,7 +138,7 @@ {{/if}}

- diff --git a/ui/tests/acceptance/init-test.js b/ui/tests/acceptance/init-test.js new file mode 100644 index 000000000000..bb2fdc543c71 --- /dev/null +++ b/ui/tests/acceptance/init-test.js @@ -0,0 +1,117 @@ +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; + +import initPage from 'vault/tests/pages/init'; +import Pretender from 'pretender'; + +const HEALTH_RESPONSE = { + initialized: false, + sealed: true, + standby: true, + performance_standby: false, + replication_performance_mode: 'unknown', + replication_dr_mode: 'unknown', + server_time_utc: 1538066726, + version: '0.11.0+prem', +}; + +const CLOUD_SEAL_RESPONSE = { + keys: [], + keys_base64: [], + recovery_keys: [ + '1659986a8d56b998b175b6e259998f3c064c061d256c2a331681b8d122fedf0db4', + '4d34c58f56e4f077e3b74f9e8db2850fc251ac3f16e952441301eedc462addeb84', + '3b3cbdf4b2f5ac1e809ff1bb72fd9778e460856561728a871a9370345bd52e97f4', + 'aa99b46e2ed5d837ee9824b7894b24987be2f32c81ab9ff5ce9e07d2012eaf4158', + 'c2bf6d71d8db8ae09b26177ed393ecb274740fe9ab51884eaa00ac113a74c08ba7', + ], + recovery_keys_base64: [ + 'FlmYao1WuZixdbbiWZmPPAZMBh0lbCozFoG40SL+3w20', + 'TTTFj1bk8Hfjt0+ejbKFD8JRrD8W6VJEEwHu3EYq3euE', + 'Ozy99LL1rB6An/G7cv2XeORghWVhcoqHGpNwNFvVLpf0', + 'qpm0bi7V2DfumCS3iUskmHvi8yyBq5/1zp4H0gEur0FY', + 'wr9tcdjbiuCbJhd+05PssnR0D+mrUYhOqgCsETp0wIun', + ], + root_token: '48dF3Drr1jl4ayM0jcHrN4NC', +}; +const SEAL_RESPONSE = { + keys: [ + '1659986a8d56b998b175b6e259998f3c064c061d256c2a331681b8d122fedf0db4', + '4d34c58f56e4f077e3b74f9e8db2850fc251ac3f16e952441301eedc462addeb84', + '3b3cbdf4b2f5ac1e809ff1bb72fd9778e460856561728a871a9370345bd52e97f4', + ], + keys_base64: [ + 'FlmYao1WuZixdbbiWZmPPAZMBh0lbCozFoG40SL+3w20', + 'TTTFj1bk8Hfjt0+ejbKFD8JRrD8W6VJEEwHu3EYq3euE', + 'Ozy99LL1rB6An/G7cv2XeORghWVhcoqHGpNwNFvVLpf0', + ], + root_token: '48dF3Drr1jl4ayM0jcHrN4NC', +}; + +const CLOUD_SEAL_STATUS_RESPONSE = { + type: 'awskms', + sealed: true, + initialized: false, +}; +const SEAL_STATUS_RESPONSE = { + type: 'shamir', + sealed: true, + initialized: false, +}; + +module('Acceptance | init', function(hooks) { + setupApplicationTest(hooks); + + let setInitResponse = (server, resp) => { + server.put('/v1/sys/init', () => { + return [200, { 'Content-Type': 'application/json' }, JSON.stringify(resp)]; + }); + }; + let setStatusResponse = (server, resp) => { + server.get('/v1/sys/seal-status', () => { + return [200, { 'Content-Type': 'application/json' }, JSON.stringify(resp)]; + }); + }; + hooks.beforeEach(function() { + this.server = new Pretender(); + this.server.get('/v1/sys/health', () => { + return [200, { 'Content-Type': 'application/json' }, JSON.stringify(HEALTH_RESPONSE)]; + }); + }); + + hooks.afterEach(function() { + this.server.shutdown(); + }); + + test('cloud seal init', async function(assert) { + setInitResponse(this.server, CLOUD_SEAL_RESPONSE); + setStatusResponse(this.server, CLOUD_SEAL_STATUS_RESPONSE); + await initPage.init(5, 3); + assert.equal( + initPage.keys.length, + CLOUD_SEAL_RESPONSE.recovery_keys.length, + 'shows all of the recovery keys' + ); + assert.equal(initPage.buttonText, 'Continue to Authenticate', 'links to authenticate'); + let { requestBody } = this.server.handledRequests.findBy('url', '/v1/sys/init'); + requestBody = JSON.parse(requestBody); + for (let attr of ['recovery_shares', 'recovery_threshold']) { + assert.ok(requestBody[attr], `requestBody includes cloud seal specific attribute: ${attr}`); + } + }); + + test('shamir seal init', async function(assert) { + setInitResponse(this.server, SEAL_RESPONSE); + setStatusResponse(this.server, SEAL_STATUS_RESPONSE); + + await initPage.init(3, 2); + assert.equal(initPage.keys.length, SEAL_RESPONSE.keys.length, 'shows all of the recovery keys'); + assert.equal(initPage.buttonText, 'Continue to Unseal', 'links to unseal'); + + let { requestBody } = this.server.handledRequests.findBy('url', '/v1/sys/init'); + requestBody = JSON.parse(requestBody); + for (let attr of ['recovery_shares', 'recovery_threshold']) { + assert.notOk(requestBody[attr], `requestBody does not include cloud seal specific attribute: ${attr}`); + } + }); +}); diff --git a/ui/tests/pages/init.js b/ui/tests/pages/init.js new file mode 100644 index 000000000000..38c5a883aac7 --- /dev/null +++ b/ui/tests/pages/init.js @@ -0,0 +1,16 @@ +import { text, create, collection, visitable, fillable, clickable } from 'ember-cli-page-object'; + +export default create({ + visit: visitable('/vault/init'), + submit: clickable('[data-test-init-submit]'), + shares: fillable('[data-test-key-shares]'), + threshold: fillable('[data-test-key-threshold]'), + keys: collection('[data-test-key-box]'), + buttonText: text('[data-test-advance-button]'), + init: async function(shares, threshold) { + await this.visit(); + return this.shares(shares) + .threshold(threshold) + .submit(); + }, +}); From 4d86dff3d4549804b92008c46ac7cfbbc0c30158 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Thu, 27 Sep 2018 15:36:35 -0500 Subject: [PATCH 6/9] stored_shares should always be 1 --- ui/app/controllers/vault/cluster/init.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/app/controllers/vault/cluster/init.js b/ui/app/controllers/vault/cluster/init.js index 55425387dfa9..4ccad76842c6 100644 --- a/ui/app/controllers/vault/cluster/init.js +++ b/ui/app/controllers/vault/cluster/init.js @@ -41,12 +41,12 @@ export default Controller.extend(DEFAULTS, { actions: { initCluster(data) { - let isCloudSeal = this.model.sealType !== 'shamir'; + let isCloudSeal = !!this.model.sealType && this.model.sealType !== 'shamir'; if (data.secret_shares) { let shares = parseInt(data.secret_shares, 10); data.secret_shares = shares; if (isCloudSeal) { - data.stored_shares = shares; + data.stored_shares = 1; data.recovery_shares = shares; } } From 8bbcdb6cdb1e4541a6a48950d7fff32736df8967 Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Thu, 27 Sep 2018 16:34:56 -0500 Subject: [PATCH 7/9] fix lint --- ui/app/models/node.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/models/node.js b/ui/app/models/node.js index e70cbf3563c6..0e18d965363c 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -1,4 +1,3 @@ -import { computed } from '@ember/object'; import { alias, and, equal } from '@ember/object/computed'; import DS from 'ember-data'; const { attr } = DS; From 66cdab130d466857f061a07b81fa0f7742939adf Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Thu, 27 Sep 2018 22:12:58 -0500 Subject: [PATCH 8/9] format template --- ui/app/templates/vault/cluster/init.hbs | 136 ++++++++++++++++++------ 1 file changed, 104 insertions(+), 32 deletions(-) diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs index 4d4f722df84b..f6a50ff33572 100644 --- a/ui/app/templates/vault/cluster/init.hbs +++ b/ui/app/templates/vault/cluster/init.hbs @@ -1,15 +1,16 @@ {{#if keyData}} - {{#with (or keyData.recovery_keys keyData.keys) as |keyArray|}} + {{#let (or keyData.recovery_keys keyData.keys) as |keyArray|}}

- Vault has been initialized! {{#if (eq keyArray.length 1)}} + Vault has been initialized! + {{#if (eq keyArray.length 1)}} Here is your key. {{else}} Here are your {{pluralize keyArray.length "key"}}. {{/if}}

- {{/with}} + {{/let}}
@@ -29,8 +30,14 @@ {{/if}}

-
- +
+

Initial Root Token @@ -38,8 +45,12 @@ {{keyData.root_token}}

- {{#each (or keyData.recovery_keys_base64 keyData.recovery_keys keyData.keys_base64 keyData.keys) as |key index| }} -
+ {{#each (or keyData.recovery_keys_base64 keyData.recovery_keys keyData.keys_base64 keyData.keys) as |key index|}} +

@@ -53,20 +64,36 @@
{{#if (and model.sealed (not keyData.recovery_keys))}} -
+
{{#link-to 'vault.cluster.unseal' model.name class="button is-primary"}} Continue to Unseal {{/link-to}}
{{else}} -
- {{#link-to 'vault.cluster.auth' model.name class=(concat (if model.sealed 'is-loading ' '') 'button is-primary') disabled=model.sealed}} +
+ {{#link-to 'vault.cluster.auth' + model.name + class=(concat (if model.sealed 'is-loading ' '') 'button is-primary') + disabled=model.sealed + }} Continue to Authenticate {{/link-to}}
{{/if}} - + Download Keys
@@ -81,67 +108,112 @@
+ id="init" + >
-
-
- - + {{#if use_pgp}}

The output unseal keys will be encrypted and hex-encoded, in order, with the given public keys.

- +
{{/if}} - + {{#if use_pgp_for_root}}

The root unseal key will be encrypted and hex-encoded with the given public key.

- +
{{/if}}
- -
{{partial "svg/initialize"}}
From 1cbac243b1a3018ce78d41b28342e995dd28bcab Mon Sep 17 00:00:00 2001 From: Matthew Irish Date: Fri, 28 Sep 2018 09:23:20 -0500 Subject: [PATCH 9/9] remove explicity model attr from init controller --- ui/app/controllers/vault/cluster/init.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/controllers/vault/cluster/init.js b/ui/app/controllers/vault/cluster/init.js index 4ccad76842c6..cc807872c018 100644 --- a/ui/app/controllers/vault/cluster/init.js +++ b/ui/app/controllers/vault/cluster/init.js @@ -13,7 +13,6 @@ const DEFAULTS = { export default Controller.extend(DEFAULTS, { wizard: service(), - model: null, reset() { this.setProperties(DEFAULTS);