Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI - ent init #5428

Merged
merged 9 commits into from
Sep 28, 2018
Merged
19 changes: 17 additions & 2 deletions ui/app/controllers/vault/cluster/init.js
Expand Up @@ -13,6 +13,7 @@ const DEFAULTS = {

export default Controller.extend(DEFAULTS, {
wizard: service(),
model: null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol no, habit from components


reset() {
this.setProperties(DEFAULTS);
Expand Down Expand Up @@ -40,15 +41,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;
}
Expand Down
5 changes: 4 additions & 1 deletion ui/app/machines/tutorial-machine.js
Expand Up @@ -34,7 +34,10 @@ export default {
onEntry: { type: 'render', level: 'feature', component: 'wizard/init-setup' },
},
save: {
on: { TOUNSEAL: 'unseal' },
on: {
TOUNSEAL: 'unseal',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These look slightly weird, like they should have an _ in them, is this due to some sort of restriction, or just then way you've gone? (wheres a 🦭 🌊 emoji when you need one 😂)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah this was more keeping in line with what's there already - I'll make a note to add underscores later as we've got more work to do on the on boarding stuff.

TOLOGIN: 'login',
},
onEntry: { type: 'render', level: 'feature', component: 'wizard/init-save-keys' },
},
unseal: {
Expand Down
6 changes: 2 additions & 4 deletions ui/app/models/cluster.js
Expand Up @@ -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);
Expand All @@ -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'
Expand Down
6 changes: 1 addition & 5 deletions 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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but you ought to be able to use modules for Ember Data as well now.

import Model from 'ember-data/model';
import attr from 'ember-data/attr';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh I guess the codemod doesn't do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oddly the guides still show the old way - gonna hold off on this now and do it all at once in a different PR.

Expand All @@ -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;
}),
});
92 changes: 42 additions & 50 deletions ui/app/templates/vault/cluster/init.hbs
@@ -1,20 +1,32 @@
<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>
{{#with (or keyData.recovery_keys keyData.keys) as |keyArray|}}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe with is deprecated in favor of let now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooo and it can yield multiple items 😙👌

<h1 class="title is-4">
Vault has been initialized! {{#if (eq keyArray.length 1)}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any harm in moving the conditional to the next line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope! I'lld do that

Here is your key.
{{else}}
Here are your {{pluralize keyArray.length "key"}}.
{{/if}}
</h1>
{{/with}}
</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">
Expand All @@ -26,8 +38,8 @@
<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">
Expand All @@ -40,27 +52,21 @@
</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}}
<DownloadButton
@data={{keyData}}
@filename={{keyFilename}}
@mime="application/json"
@extension="json"
@class="button is-ghost"
@stringify={{true}}
>
<DownloadButton @data={{keyData}} @filename={{keyFilename}} @mime="application/json" @extension="json" @class="button is-ghost"
@stringify={{true}}>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is some weird formatting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh yes js-beautify's default which VSCode uses does "break attrs only after max line length" by default - fixed it to break all attrs which is also kinda weird, but more easily read imo.

<ICon @glyph="download" @size=16 /> Download Keys
</DownloadButton>
</div>
Expand All @@ -73,7 +79,8 @@
</h1>
</Page.header>
<Page.content>
<form {{action 'initCluster' (hash
<form
{{action 'initCluster' (hash
secret_shares=secret_shares
secret_threshold=secret_threshold
pgp_keys=pgp_keys
Expand All @@ -83,16 +90,15 @@
)
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">
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
Expand All @@ -103,20 +109,15 @@
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"
/>
<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">
Expand All @@ -125,13 +126,8 @@
<PgpList @listLength={{secret_shares}} @onDataUpdate={{action 'setKeys'}} />
</div>
{{/if}}
<ToggleButton
@openLabel="Encrypt Root Token with PGP"
@closedLabel="Encrypt Root Token with PGP"
@toggleTarget={{this}}
@toggleAttr="use_pgp_for_root"
@class="is-block"
/>
<ToggleButton @openLabel="Encrypt Root Token with PGP" @closedLabel="Encrypt Root Token with PGP"
@toggleTarget={{this}} @toggleAttr="use_pgp_for_root" @class="is-block" />
{{#if use_pgp_for_root}}
<div class="box init-box">
<p class="help has-text-grey">
Expand All @@ -142,11 +138,7 @@
{{/if}}
</div>
<div class="box is-marginless is-shadowless">
<button
type="submit"
class="button is-primary {{if loading 'is-loading'}}"
disabled={{loading}}
>
<button data-test-init-submit type="submit" class="button is-primary {{if loading 'is-loading'}}" disabled={{loading}}>
Initialize
</button>

Expand All @@ -157,4 +149,4 @@
</form>
</Page.content>
{{/if}}
</SplashPage>
</SplashPage>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional lack of newline or fallout from switching to VSCode 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱 also another VSCode thing which I think I've fixed...

117 changes: 117 additions & 0 deletions 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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these helpers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks! maybe overkill for 2 tests, but felt like it cleaned things up nicely

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}`);
}
});
});