- Unseal Vault by entering portions of the unseal key. This can be done via multiple mechanisms on multiple
- computers. Once all portions are entered, the root key will be decrypted and Vault will unseal.
-
+ {{#if this.model.unsealed}}
+
Please wait while we redirect you.
+ {{else}}
+
+ Unseal Vault by entering portions of the unseal key. This can be done via multiple mechanisms on multiple
+ computers. Once all portions are entered, the root key will be decrypted and Vault will unseal.
+
+
+ {{/if}}
-
diff --git a/ui/lib/core/addon/components/choose-pgp-key-form.hbs b/ui/lib/core/addon/components/choose-pgp-key-form.hbs
new file mode 100644
index 0000000000..e6ae1580b6
--- /dev/null
+++ b/ui/lib/core/addon/components/choose-pgp-key-form.hbs
@@ -0,0 +1,64 @@
+{{#if this.selectedPgp}}
+
+{{else}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/choose-pgp-key-form.js b/ui/lib/core/addon/components/choose-pgp-key-form.js
new file mode 100644
index 0000000000..eeb7916b86
--- /dev/null
+++ b/ui/lib/core/addon/components/choose-pgp-key-form.js
@@ -0,0 +1,53 @@
+import { action } from '@ember/object';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+const pgpKeyFileDefault = () => ({ value: '' });
+
+/**
+ * @module ChoosePgpKeyForm
+ * ChoosePgpKeyForm component is used for DR Operation Token Generation workflow. It provides
+ * an interface for the user to upload or paste a PGP key for use
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {function} onCancel - required - This function will be triggered when the modal intends to be closed
+ * @param {function} onSubmit - required - When the PGP key is confirmed, it will call this method with the pgpKey value as the only param
+ * @param {string} buttonText - Button text for onSubmit. Defaults to "Continue with key"
+ * @param {string} formText - Form text above where the users uploads or pastes the key. Has default
+ */
+export default class ChoosePgpKeyForm extends Component {
+ @tracked pgpKeyFile = pgpKeyFileDefault();
+ @tracked selectedPgp = '';
+
+ get pgpKey() {
+ return this.pgpKeyFile.value;
+ }
+
+ get buttonText() {
+ return this.args.buttonText || 'Continue with key';
+ }
+
+ get formText() {
+ return (
+ this.args.formText ||
+ 'Choose a PGP Key from your computer or paste the contents of one in the form below.'
+ );
+ }
+
+ @action setKey(_, keyFile) {
+ this.pgpKeyFile = keyFile;
+ }
+
+ // Form submit actions:
+ @action usePgpKey(evt) {
+ evt.preventDefault();
+ this.selectedPgp = this.pgpKey;
+ }
+ @action handleSubmit(evt) {
+ evt.preventDefault();
+ this.args.onSubmit(this.pgpKey);
+ }
+}
diff --git a/ui/lib/core/addon/components/shamir-flow.js b/ui/lib/core/addon/components/shamir-flow.js
deleted file mode 100644
index 7346078493..0000000000
--- a/ui/lib/core/addon/components/shamir-flow.js
+++ /dev/null
@@ -1,192 +0,0 @@
-/**
- * Copyright (c) HashiCorp, Inc.
- * SPDX-License-Identifier: MPL-2.0
- */
-
-import { inject as service } from '@ember/service';
-import { gt } from '@ember/object/computed';
-import { camelize } from '@ember/string';
-import Component from '@ember/component';
-import { get, computed } from '@ember/object';
-import layout from '../templates/components/shamir-flow';
-import { A } from '@ember/array';
-
-const pgpKeyFileDefault = () => ({ value: '' });
-const DEFAULTS = {
- key: null,
- loading: false,
- errors: A(),
- threshold: null,
- progress: null,
- pgp_key: null,
- haveSavedPGPKey: false,
- started: false,
- generateWithPGP: false,
- pgpKeyFile: pgpKeyFileDefault(),
- nonce: '',
-};
-
-export default Component.extend(DEFAULTS, {
- tagName: '',
- store: service(),
- formText: null,
- fetchOnInit: false,
- buttonText: 'Submit',
- thresholdPath: 'required',
- generateAction: false,
- layout,
-
- init() {
- this._super(...arguments);
- if (this.fetchOnInit) {
- this.attemptProgress();
- }
- },
-
- didInsertElement() {
- this._super(...arguments);
- this.onUpdate(this.getProperties(Object.keys(DEFAULTS)));
- },
-
- onUpdate() {},
- onLicenseError() {},
- onShamirSuccess() {},
- // can be overridden w/an attr
- isComplete(data) {
- return data.complete === true;
- },
-
- stopLoading() {
- this.setProperties({
- loading: false,
- errors: [],
- key: null,
- });
- },
-
- reset() {
- this.setProperties(DEFAULTS);
- },
-
- hasProgress: gt('progress', 0),
-
- actionSuccess(resp) {
- const { onUpdate, isComplete, onShamirSuccess, thresholdPath } = this;
- const threshold = get(resp, thresholdPath);
- const props = {
- ...resp,
- threshold,
- };
- this.stopLoading();
- // if we have an OTP, but update doesn't include one,
- // we don't want to null it out
- if (this.otp && !props.otp) {
- delete props.otp;
- }
- this.setProperties(props);
- onUpdate(props);
- if (isComplete(props)) {
- this.reset();
- onShamirSuccess(props);
- }
- },
-
- actionError(e) {
- this.stopLoading();
- if (e.httpStatus === 400) {
- this.set('errors', e.errors);
- } else {
- // if licensing error, trigger parent method to handle
- if (e.httpStatus === 500 && e.errors?.join(' ').includes('licensing is in an invalid state')) {
- this.onLicenseError();
- }
- throw e;
- }
- },
-
- generateStep: computed('generateWithPGP', 'haveSavedPGPKey', 'pgp_key', function () {
- const { generateWithPGP, pgp_key, haveSavedPGPKey } = this;
- if (!generateWithPGP && !pgp_key) {
- return 'chooseMethod';
- }
- if (generateWithPGP) {
- if (pgp_key && haveSavedPGPKey) {
- return 'beginGenerationWithPGP';
- } else {
- return 'providePGPKey';
- }
- }
- return '';
- }),
-
- extractData(data) {
- const isGenerate = this.generateAction;
- const hasStarted = this.started;
- const usePGP = this.generateWithPGP;
- const nonce = this.nonce;
-
- if (!isGenerate || hasStarted) {
- if (nonce) {
- data.nonce = nonce;
- }
- return data;
- }
-
- if (usePGP) {
- return {
- pgp_key: data.pgp_key,
- };
- }
-
- return {
- attempt: data.attempt,
- };
- },
-
- attemptProgress(data) {
- const checkStatus = data ? false : true;
- let action = this.action;
- action = action && camelize(action);
- this.set('loading', true);
- const adapter = this.store.adapterFor('cluster');
- const method = adapter[action];
-
- method.call(adapter, data, { checkStatus }).then(
- (resp) => this.actionSuccess(resp),
- (...args) => this.actionError(...args)
- );
- },
-
- actions: {
- reset() {
- this.reset();
- this.set('encoded_token', null);
- this.set('otp', null);
- },
-
- onSubmit(data) {
- if (!data.key) {
- return;
- }
- this.attemptProgress(this.extractData(data));
- },
-
- startGenerate(data) {
- if (this.generateAction) {
- data.attempt = true;
- }
- this.attemptProgress(this.extractData(data));
- },
-
- setKey(_, keyFile) {
- this.set('pgp_key', keyFile.value);
- this.set('pgpKeyFile', keyFile);
- },
-
- savePGPKey() {
- if (this.pgp_key) {
- this.set('haveSavedPGPKey', true);
- }
- },
- },
-});
diff --git a/ui/lib/core/addon/components/shamir-modal-flow.js b/ui/lib/core/addon/components/shamir-modal-flow.js
deleted file mode 100644
index a36f039b12..0000000000
--- a/ui/lib/core/addon/components/shamir-modal-flow.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * Copyright (c) HashiCorp, Inc.
- * SPDX-License-Identifier: MPL-2.0
- */
-
-/**
- * @module ShamirModalFlow
- * ShamirModalFlow is an extension of the ShamirFlow component that does the Generate Action Token workflow inside of a Modal.
- * Please note, this is not an extensive list of the required parameters -- please see ShamirFlow for others
- *
- * @example
- * ```js
- * This copy is the main paragraph when the token flow has not started
- * ```
- * @param {function} onClose - This function will be triggered when the modal intends to be closed
- */
-import { inject as service } from '@ember/service';
-import ShamirFlow from './shamir-flow';
-import layout from '../templates/components/shamir-modal-flow';
-
-export default ShamirFlow.extend({
- layout,
- store: service(),
- onClose: () => {},
- actions: {
- onCancelClose() {
- if (this.encoded_token) {
- this.send('reset');
- } else if (this.generateAction && !this.started) {
- if (this.generateStep !== 'chooseMethod') {
- this.send('reset');
- }
- } else {
- const adapter = this.store.adapterFor('cluster');
- adapter.generateDrOperationToken(this.model, { cancel: true });
- this.send('reset');
- }
- this.onClose();
- },
- onClose() {
- this.onClose();
- },
- },
-});
diff --git a/ui/lib/core/addon/components/shamir/dr-token-flow.hbs b/ui/lib/core/addon/components/shamir/dr-token-flow.hbs
new file mode 100644
index 0000000000..019de46032
--- /dev/null
+++ b/ui/lib/core/addon/components/shamir/dr-token-flow.hbs
@@ -0,0 +1,135 @@
+
+ {{#if this.encodedToken}}
+
+ Below is the process and the values necessary to generate your operation token. Read the instructions carefully!
+
+
+
+
+ Encoded operation token
+
+
+ This is a one-time token that will be used to generate the operation token. Please save it.
+
+
+
+ {{this.encodedToken}}
+
+
+ {{#if this.otp}}
+
+
+ One time password (OTP)
+
+
+ This OTP will be used to decode the generated operation token. Please save it.
+
+
+
+ {{this.otp}}
+
+
+ {{/if}}
+
+
+ DR operation token command
+
+
+ {{#if this.otp}}
+ This command contains both the encoded token and the OTP. It should be executed on the secondary cluster in order
+ to generate the operation token.
+ {{else}}
+ This command requires the OTP saved earlier. It should be executed on the secondary cluster in order to generate
+ the operation token.
+ {{/if}}
+
Generate an operation token by entering a portion of the
+ primary's root key. Once all portions are entered, the generated token may be used to manage your
+ secondary Disaster Recovery cluster.
+
+
+ {{else if this.generateWithPGP}}
+
+ {{else}}
+ {{! Generate token flow not started }}
+
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/shamir/dr-token-flow.js b/ui/lib/core/addon/components/shamir/dr-token-flow.js
new file mode 100644
index 0000000000..a31a4669e1
--- /dev/null
+++ b/ui/lib/core/addon/components/shamir/dr-token-flow.js
@@ -0,0 +1,126 @@
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import ShamirFlowComponent from './flow';
+
+/**
+ * @module ShamirDrTokenFlowComponent
+ * ShamirDrTokenFlow is an extension of the ShamirFlow component that does the Generate Action Token workflow inside of a Modal.
+ * Please note, this is not an extensive list of the required parameters -- please see ShamirFlow for others
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ * @param {string} action - required kebab-case-string which refers to an action within the cluster adapter
+ * @param {function} onCancel - if provided, function will be triggered on Cancel
+ */
+export default class ShamirDrTokenFlowComponent extends ShamirFlowComponent {
+ @tracked generateWithPGP = false; // controls which form shows
+ @tracked savedPgpKey = null;
+ @tracked otp = '';
+
+ constructor() {
+ super(...arguments);
+ // Fetch status on init
+ this.attemptProgress();
+ }
+
+ reset() {
+ this.generateWithPGP = false;
+ this.savedPgpKey = null;
+ this.otp = '';
+ // tracked items on Shamir/Flow
+ this.attemptResponse = null;
+ this.errors = null;
+ }
+
+ // Values calculated from the attempt response
+ get encodedToken() {
+ return this.attemptResponse?.encoded_token;
+ }
+ get started() {
+ return this.attemptResponse?.started;
+ }
+ get nonce() {
+ return this.attemptResponse?.nonce;
+ }
+ get progress() {
+ return this.attemptResponse?.progress;
+ }
+ get threshold() {
+ return this.attemptResponse?.required;
+ }
+ get pgpText() {
+ return {
+ confirm: `Below is the base-64 encoded PGP Key that will be used to encrypt the generated operation token. Next we'll enter portions of the root key to generate an operation token. Click the "Generate operation token" button to proceed.`,
+ form: `Choose a PGP Key from your computer or paste the contents of one in the form below. This key will be used to Encrypt the generated operation token.`,
+ };
+ }
+
+ // Methods which override those in Shamir/Flow
+ extractData(data) {
+ if (this.started) {
+ if (this.nonce) {
+ data.nonce = this.nonce;
+ }
+ return data;
+ }
+ if (this.savedPgpKey) {
+ return {
+ pgp_key: this.savedPgpKey,
+ };
+ }
+ // only if !started
+ return {
+ attempt: data.attempt,
+ };
+ }
+
+ updateProgress(response) {
+ if (response.otp) {
+ // OTP is sticky -- once we get one we don't want to remove it
+ // even if the current response doesn't include one.
+ // See PR #5818
+ this.otp = response.otp;
+ }
+ this.attemptResponse = response;
+ return;
+ }
+
+ @action
+ usePgpKey(keyfile) {
+ this.savedPgpKey = keyfile;
+ this.attemptProgress(this.extractData({ attempt: true }));
+ }
+
+ @action
+ startGenerate(evt) {
+ evt.preventDefault();
+ this.attemptProgress(this.extractData({ attempt: true }));
+ }
+
+ @action
+ async onCancelClose() {
+ if (!this.encodedToken && this.started) {
+ const adapter = this.store.adapterFor('cluster');
+ await adapter.generateDrOperationToken({}, { cancel: true });
+ }
+ this.reset();
+ if (this.args.onCancel) {
+ this.args.onCancel();
+ }
+ }
+}
+
+/* generate-operation-token response example
+{
+ "started": true,
+ "nonce": "2dbd10f1-8528-6246-09e7-82b25b8aba63",
+ "progress": 1,
+ "required": 3,
+ "encoded_token": "",
+ "otp": "2vPFYG8gUSW9npwzyvxXMug0",
+ "otp_length": 24,
+ "complete": false
+}
+*/
diff --git a/ui/lib/core/addon/components/shamir/flow.hbs b/ui/lib/core/addon/components/shamir/flow.hbs
new file mode 100644
index 0000000000..75aaf506de
--- /dev/null
+++ b/ui/lib/core/addon/components/shamir/flow.hbs
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/shamir/flow.js b/ui/lib/core/addon/components/shamir/flow.js
new file mode 100644
index 0000000000..303acaa471
--- /dev/null
+++ b/ui/lib/core/addon/components/shamir/flow.js
@@ -0,0 +1,168 @@
+import { A } from '@ember/array';
+import { action } from '@ember/object';
+import { service } from '@ember/service';
+import { camelize } from '@ember/string';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+/**
+ * @module ShamirFlowComponent
+ * These components are used to manage keeping track of a shamir unseal flow.
+ * This component is generic and can be overwritten for various shamir use cases.
+ * The lifecycle for a Shamir flow is as follows:
+ * 1. Start (optional)
+ * 2. Attempt progress
+ * 3. Check progress
+ * 4. Check complete
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ *
+ * @param {string} action - adapter method name (kebab case) to call on attempt
+ * @param {number} threshold - number of keys required to unlock
+ * @param {number} progress - number of keys given so far for unlock
+ * @param {string} inputLabel - (optional) Label for key input
+ * @param {string} buttonText - (optional) CTA for the form submit button. Defaults to "Submit"
+ * @param {Function} extractData - (optional) modify the payload before the action is called
+ * @param {Function} updateProgress - (optional) call a side effect to check if progress has been made
+ * @param {Function} checkComplete - (optional) custom logic based on adapter response. Should return boolean.
+ * @param {Function} onShamirSuccess - method called when shamir unlock is complete.
+ * @param {Function} onLicenseError - method called when shamir unlock fails due to licensing error
+ *
+ */
+export default class ShamirFlowComponent extends Component {
+ @service store;
+ @tracked errors = A();
+ @tracked attemptResponse = null;
+
+ get action() {
+ if (!this.args.action) return '';
+ return camelize(this.args.action);
+ }
+
+ extractData(data) {
+ if (this.args.extractData) {
+ // custom data extraction
+ return this.args.extractData(data);
+ }
+
+ // This method can be overwritten by extended components
+ // to control what data is passed into the method action
+ if (this.attemptResponse?.nonce) {
+ data.nonce = this.attemptResponse.nonce;
+ }
+ return data;
+ }
+
+ /**
+ * 2. Attempt progress. This method assumes the correct data
+ * has already been extracted (use this.extractData to customize)
+ * @param {object} data arbitrary data which will be passed to adapter method
+ * @returns Promise which should resolve unless throwing error to parent.
+ */
+ async attemptProgress(data) {
+ this.errors = null;
+ const action = this.action;
+ const adapter = this.store.adapterFor('cluster');
+ const method = adapter[action];
+ // Only used for DR token generate
+ const checkStatus = data ? false : true;
+
+ try {
+ const resp = await method.call(adapter, data, { checkStatus });
+ this.updateProgress(resp);
+ this.handleComplete(resp);
+ return;
+ } catch (e) {
+ if (e.httpStatus === 400) {
+ this.errors = e.errors;
+ return;
+ } else {
+ // if licensing error, trigger parent method to handle
+ if (e.httpStatus === 500 && e.errors?.join(' ').includes('licensing is in an invalid state')) {
+ this.args.onLicenseError();
+ }
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * 3. This method gets called after successful unseal attempt.
+ * By default the response will be made available to the component,
+ * but pass in @updateProgress (no params) to trigger any side effects that will
+ * update passed attributes from parent.
+ * @param {payload} response from the adapter method
+ * @returns void
+ */
+ updateProgress(response) {
+ if (this.args.updateProgress) {
+ this.args.updateProgress();
+ }
+ this.attemptResponse = response;
+ return;
+ }
+
+ /**
+ * 4. checkComplete checks the payload for completeness, then then
+ * takes calls @onShamirSuccess with no arguments if complete.
+ * For custom logic, define @checkComplete which receives the
+ * adapter payload.
+ * @param {payload} response from the adapter method
+ * @returns void
+ */
+ handleComplete(response) {
+ const isComplete = this.checkComplete(response);
+ if (isComplete) {
+ if (this.args.onShamirSuccess) {
+ this.args.onShamirSuccess();
+ }
+ }
+ return;
+ }
+
+ checkComplete(response) {
+ if (this.args.checkComplete) {
+ return this.args.checkComplete(response);
+ }
+ return response.complete === true;
+ }
+
+ reset() {
+ this.attemptResponse = null;
+ this.errors = null;
+ }
+
+ @action
+ onSubmitKey(data) {
+ this.attemptProgress(this.extractData(data));
+ }
+}
+
+/* example unseal response (progress)
+{
+ "sealed": true,
+ "t": 3,
+ "n": 5,
+ "progress": 2,
+ "version": "0.6.2"
+}
+
+example unseal response (finished)
+{
+ "sealed": false,
+ "t": 3,
+ "n": 5,
+ "progress": 0,
+ "version": "0.6.2",
+ "cluster_name": "vault-cluster-d6ec3c7f",
+ "cluster_id": "3e8b3fec-3749-e056-ba41-b62a63b997e8"
+}
+*/
diff --git a/ui/lib/core/addon/components/shamir/form.hbs b/ui/lib/core/addon/components/shamir/form.hbs
new file mode 100644
index 0000000000..bfe99d718b
--- /dev/null
+++ b/ui/lib/core/addon/components/shamir/form.hbs
@@ -0,0 +1,48 @@
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/shamir/form.js b/ui/lib/core/addon/components/shamir/form.js
new file mode 100644
index 0000000000..949adf72cc
--- /dev/null
+++ b/ui/lib/core/addon/components/shamir/form.js
@@ -0,0 +1,61 @@
+import { action } from '@ember/object';
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+/**
+ * @module ShamirFormComponent
+ * These components are used to make progress against a Shamir seal.
+ * Depending on the response, and external polling, the component will show
+ * progress and optional
+ *
+ * @example
+ * ```js
+ *
+ * ```
+ *
+ * @param {Function} onSubmit - method which handles the action for shamir. Receives { key }
+ * @param {number} threshold - min number of keys required to unlock shamir seal
+ * @param {number} progress - number of keys given so far for unlock
+ * @param {string} buttonText - CTA for the form submit button. Defaults to "Submit"
+ * @param {string} inputLabel - Label for key input. Defaults to "Shamir key portion"
+ * @param {boolean} alwaysShowProgress - determines if the shamir progress should always show, or only when > 0 progress
+ * @param {string} otp - if otp is present, it will show a section describing what to do with it
+ *
+ */
+export default class ShamirFormComponent extends Component {
+ @tracked key = '';
+ @tracked loading = false;
+
+ get buttonText() {
+ return this.args.buttonText || 'Submit';
+ }
+ get showProgress() {
+ return this.args.progress > 0 || this.args.alwaysShowProgress;
+ }
+ get inputLabel() {
+ return this.args.inputLabel || 'Shamir key portion';
+ }
+
+ resetForm() {
+ this.key = '';
+ this.loading = false;
+ }
+
+ @action
+ async onSubmit(key, evt) {
+ evt.preventDefault();
+
+ if (!key) {
+ return;
+ }
+ // Parent handles action and passes in errors if present
+ await this.args.onSubmit({ key });
+ this.resetForm();
+ }
+}
diff --git a/ui/lib/core/addon/templates/components/replication-action-generate-token.hbs b/ui/lib/core/addon/templates/components/replication-action-generate-token.hbs
index 2c6c1bfb1f..b17a8b441d 100644
--- a/ui/lib/core/addon/templates/components/replication-action-generate-token.hbs
+++ b/ui/lib/core/addon/templates/components/replication-action-generate-token.hbs
@@ -20,17 +20,16 @@
-
-
- Updating or promoting this cluster requires an operation token, generated by inputting the root key shares. If you'd like
- to first encrypt the token with a PGP Key, click "Encrypt with PGP key" below, otherwise we can begin generation of the
- operation token.
-
-
\ No newline at end of file
+ {{#if this.isModalActive}}
+ {{! Wrapped in if statement so the Shamir constructor fires on modal open }}
+
+ {{! Section & Footer is in child component since the form must do side effects on cancel }}
+ {{/if}}
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/templates/components/shamir-modal-flow.hbs b/ui/lib/core/addon/templates/components/shamir-modal-flow.hbs
deleted file mode 100644
index b4b4d046b9..0000000000
--- a/ui/lib/core/addon/templates/components/shamir-modal-flow.hbs
+++ /dev/null
@@ -1,219 +0,0 @@
-
-
- {{#if this.encoded_token}}
-
- Below is the process and the values necessary to generate your operation token. Read the instructions carefully!
-
-
-
-
- Encoded operation token
-
-
- This is a one-time token that will be used to generate the operation token. Please save it.
-
-
-
- {{this.encoded_token}}
-
-
- {{#if this.otp}}
-
-
- One time password (OTP)
-
-
- This OTP will be used to decode the generated operation token. Please save it.
-
-
-
- {{this.otp}}
-
-
- {{/if}}
-
-
- DR operation token command
-
-
- {{#if this.otp}}
- This command contains both the encoded token and the OTP. It should be executed on the secondary cluster in
- order to generate the operation token.
- {{else}}
- This command requires the OTP saved earlier. It should be executed on the secondary cluster in order to
- generate the operation token.
- {{/if}}
-