diff --git a/ui/app/controllers/vault/cluster/init.js b/ui/app/controllers/vault/cluster/init.js index ae6a66c2e4e9..cc807872c018 100644 --- a/ui/app/controllers/vault/cluster/init.js +++ b/ui/app/controllers/vault/cluster/init.js @@ -40,15 +40,29 @@ export default Controller.extend(DEFAULTS, { actions: { initCluster(data) { + let isCloudSeal = !!this.model.sealType && this.model.sealType !== 'shamir'; if (data.secret_shares) { - data.secret_shares = parseInt(data.secret_shares); + let shares = parseInt(data.secret_shares, 10); + data.secret_shares = shares; + if (isCloudSeal) { + data.stored_shares = 1; + 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; + if (isCloudSeal) { + data.recovery_threshold = threshold; + } } if (!data.use_pgp) { delete data.pgp_keys; } + if (data.use_pgp && isCloudSeal) { + data.recovery_pgp_keys = data.pgp_keys; + } + if (!data.use_pgp_for_root) { delete data.root_token_pgp_key; } 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: { 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..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; @@ -24,13 +23,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; - }), }); diff --git a/ui/app/templates/vault/cluster/init.hbs b/ui/app/templates/vault/cluster/init.hbs index b3be6a9a07e9..f6a50ff33572 100644 --- a/ui/app/templates/vault/cluster/init.hbs +++ b/ui/app/templates/vault/cluster/init.hbs @@ -1,24 +1,43 @@ {{#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}} -

+ {{#let (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}} +

+ {{/let}}

- 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}}

-
- +
+

Initial Root Token @@ -26,8 +45,12 @@ {{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|}} +

@@ -40,16 +63,26 @@

- {{#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 + Continue to Unseal {{/link-to}}
{{else}} -
- {{#link-to 'vault.cluster.auth' model.name class="button is-primary"}} - Continue to Authenticate +
+ {{#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}} @@ -60,7 +93,7 @@ @extension="json" @class="button is-ghost" @stringify={{true}} - > + > Download Keys
@@ -73,56 +106,84 @@ -
-
-
- + /> {{#if use_pgp}}

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

- +
{{/if}} The root unseal key will be encrypted and hex-encoded with the given public key.

- +
{{/if}}
-
{{partial "svg/initialize"}}
@@ -157,4 +221,4 @@ {{/if}} - + \ No newline at end of file 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(); + }, +});