UI - ent init (#5428)
* allow for enterprise init attributes * allow moving from init to auth in the init flow on the tutorial machine * show loading spinner while cluster is unsealing * use seal-status type to determine the init attrs * add init acceptance tests * stored_shares should always be 1 * fix lint * format template * remove explicity model attr from init controller
This commit is contained in:
parent
f7bf4a4384
commit
a22861cee9
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,24 +1,43 @@
|
|||
<SplashPage as |Page|>
|
||||
{{#if keyData}}
|
||||
<Page.header>
|
||||
<h1 class="title is-4">
|
||||
Vault has been initialized! {{#if (eq keyData.keys.length 1)}}
|
||||
Here is your key.
|
||||
{{else}}
|
||||
Here are your {{pluralize keyData.keys.length "key"}}.
|
||||
{{/if}}
|
||||
</h1>
|
||||
{{#let (or keyData.recovery_keys keyData.keys) as |keyArray|}}
|
||||
<h1 class="title is-4">
|
||||
Vault has been initialized!
|
||||
{{#if (eq keyArray.length 1)}}
|
||||
Here is your key.
|
||||
{{else}}
|
||||
Here are your {{pluralize keyArray.length "key"}}.
|
||||
{{/if}}
|
||||
</h1>
|
||||
{{/let}}
|
||||
</Page.header>
|
||||
<Page.content>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<div class="content">
|
||||
<p>
|
||||
Please securely distribute the keys below. When the Vault is re-sealed, restarted, or stopped, you must provide at least <strong class="has-text-danger">{{secret_threshold}}</strong> of these keys to unseal it again.
|
||||
Vault does not store the master key. Without at least <strong class="has-text-danger">{{secret_threshold}}</strong> 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 <strong class="has-text-danger">{{secret_threshold}}</strong> 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 <strong class="has-text-danger">{{secret_threshold}}</strong> of these keys to unseal it
|
||||
again.
|
||||
Vault does not store the master key. Without at least <strong class="has-text-danger">{{secret_threshold}}</strong>
|
||||
keys, your Vault will remain permanently sealed.
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="message is-list is-highlight has-copy-button" tabindex="-1">
|
||||
<HoverCopyButton @alwaysShow=true @copyValue={{keyData.root_token}} />
|
||||
<div
|
||||
class="message is-list is-highlight has-copy-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<HoverCopyButton
|
||||
@alwaysShow=true
|
||||
@copyValue={{keyData.root_token}}
|
||||
/>
|
||||
<div class="message-body">
|
||||
<h4 class="title is-7 is-marginless">
|
||||
Initial Root Token
|
||||
|
@ -26,8 +45,12 @@
|
|||
<code class="is-word-break">{{keyData.root_token}}</code>
|
||||
</div>
|
||||
</div>
|
||||
{{#each (if keyData.keys_base64 keyData.keys_base64 keyData.keys) as |key index| }}
|
||||
<div class="message is-list has-copy-button" tabindex="-1">
|
||||
{{#each (or keyData.recovery_keys_base64 keyData.recovery_keys keyData.keys_base64 keyData.keys) as |key index|}}
|
||||
<div
|
||||
data-test-key-box
|
||||
class="message is-list has-copy-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<HoverCopyButton @copyValue={{key}} />
|
||||
<div class="message-body">
|
||||
<h4 class="title is-7 is-marginless">
|
||||
|
@ -40,16 +63,26 @@
|
|||
</div>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<div class="field is-grouped-split">
|
||||
{{#if model.sealed}}
|
||||
<div class="control">
|
||||
{{#if (and model.sealed (not keyData.recovery_keys))}}
|
||||
<div
|
||||
data-test-advance-button
|
||||
class="control"
|
||||
>
|
||||
{{#link-to 'vault.cluster.unseal' model.name class="button is-primary"}}
|
||||
Continue to Unseal
|
||||
Continue to Unseal
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="control">
|
||||
{{#link-to 'vault.cluster.auth' model.name class="button is-primary"}}
|
||||
Continue to Authenticate
|
||||
<div
|
||||
data-test-advance-button
|
||||
class="control"
|
||||
>
|
||||
{{#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}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -60,7 +93,7 @@
|
|||
@extension="json"
|
||||
@class="button is-ghost"
|
||||
@stringify={{true}}
|
||||
>
|
||||
>
|
||||
<ICon @glyph="download" @size=16 /> Download Keys
|
||||
</DownloadButton>
|
||||
</div>
|
||||
|
@ -73,56 +106,84 @@
|
|||
</h1>
|
||||
</Page.header>
|
||||
<Page.content>
|
||||
<form {{action 'initCluster' (hash
|
||||
secret_shares=secret_shares
|
||||
secret_threshold=secret_threshold
|
||||
pgp_keys=pgp_keys
|
||||
use_pgp=use_pgp
|
||||
use_pgp_for_root=use_pgp_for_root
|
||||
root_token_pgp_key=root_token_pgp_key
|
||||
<form
|
||||
{{action 'initCluster' (hash
|
||||
secret_shares=secret_shares
|
||||
secret_threshold=secret_threshold
|
||||
pgp_keys=pgp_keys
|
||||
use_pgp=use_pgp
|
||||
use_pgp_for_root=use_pgp_for_root
|
||||
root_token_pgp_key=root_token_pgp_key
|
||||
)
|
||||
on="submit"
|
||||
}}
|
||||
id="init"
|
||||
id="init"
|
||||
>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<MessageError @errors={{errors}} />
|
||||
<div class="field">
|
||||
<label for="key-shares" class="is-label">
|
||||
<label
|
||||
for="key-shares"
|
||||
class="is-label"
|
||||
>
|
||||
Key Shares
|
||||
</label>
|
||||
<div class="control">
|
||||
{{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
|
||||
}}
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
The number of key shares to split the master key into
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="key-threshold" class="is-label">
|
||||
<label
|
||||
for="key-threshold"
|
||||
class="is-label"
|
||||
>
|
||||
Key Threshold
|
||||
</label>
|
||||
<div class="control">
|
||||
{{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
|
||||
}}
|
||||
</div>
|
||||
<p class="help has-text-grey">
|
||||
The number of key shares required to reconstruct the master key
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ToggleButton
|
||||
@openLabel="Encrypt Output with PGP"
|
||||
@closedLabel="Encrypt Output with PGP"
|
||||
@toggleTarget={{this}}
|
||||
@toggleAttr="use_pgp"
|
||||
@class="is-block"
|
||||
/>
|
||||
/>
|
||||
{{#if use_pgp}}
|
||||
<div class="box init-box">
|
||||
<p class="help has-text-grey">
|
||||
The output unseal keys will be encrypted and hex-encoded, in order, with the given public keys.
|
||||
</p>
|
||||
<PgpList @listLength={{secret_shares}} @onDataUpdate={{action 'setKeys'}} />
|
||||
<PgpList
|
||||
@listLength={{secret_shares}}
|
||||
@onDataUpdate={{action 'setKeys'}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<ToggleButton
|
||||
|
@ -137,19 +198,22 @@
|
|||
<p class="help has-text-grey">
|
||||
The root unseal key will be encrypted and hex-encoded with the given public key.
|
||||
</p>
|
||||
<PgpList @listLength=1 @onDataUpdate={{action 'setRootKey'}} />
|
||||
<PgpList
|
||||
@listLength=1
|
||||
@onDataUpdate={{action 'setRootKey'}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="box is-marginless is-shadowless">
|
||||
<button
|
||||
data-test-init-submit
|
||||
type="submit"
|
||||
class="button is-primary {{if loading 'is-loading'}}"
|
||||
disabled={{loading}}
|
||||
>
|
||||
Initialize
|
||||
</button>
|
||||
|
||||
<div class="init-illustration">
|
||||
{{partial "svg/initialize"}}
|
||||
</div>
|
||||
|
@ -157,4 +221,4 @@
|
|||
</form>
|
||||
</Page.content>
|
||||
{{/if}}
|
||||
</SplashPage>
|
||||
</SplashPage>
|
|
@ -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}`);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue