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: download generated pki key #18381

Merged
merged 10 commits into from
Dec 15, 2022
3 changes: 2 additions & 1 deletion ui/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default class App extends Application {
dependencies: {
services: [
'auth',
'download',
'flash-messages',
'namespace',
'path-help',
Expand All @@ -57,10 +58,10 @@ export default class App extends Application {
'namespace',
'path-help',
'router',
'secret-mount-path',
'store',
'version',
'wizard',
'secret-mount-path',
],
externalRoutes: {
secrets: 'vault.cluster.secrets.backends',
Expand Down
53 changes: 39 additions & 14 deletions ui/app/services/download.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
import Service from '@ember/service';

interface Extensions {
csv: string;
hcl: string;
sentinel: string;
json: string;
pem: string;
txt: string;
}

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
const EXTENSION_TO_MIME: Extensions = {
csv: 'txt/csv',
hcl: 'text/plain',
sentinel: 'text/plain',
json: 'application/json',
pem: 'application/x-pem-file',
txt: 'text/plain',
};

export default class DownloadService extends Service {
download(filename: string, mimetype: string, content: string) {
download(filename: string, content: string, extension: string) {
// replace spaces with hyphens, append extension to filename
const formattedFilename =
`${filename?.replace(/\s+/g, '-')}.${extension}` ||
`vault-data-${new Date().toISOString()}.${extension}`;

// map extension to MIME type or use default
const mimetype = EXTENSION_TO_MIME[extension as keyof Extensions] || 'text/plain';

// commence download
const { document, URL } = window;
const downloadElement = document.createElement('a');
downloadElement.download = filename;
downloadElement.href = URL.createObjectURL(
new Blob([content], {
type: mimetype,
})
);
const data = new File([content], formattedFilename, { type: mimetype });
downloadElement.download = formattedFilename;
downloadElement.href = URL.createObjectURL(data);
document.body.appendChild(downloadElement);
downloadElement.click();
URL.revokeObjectURL(downloadElement.href);
downloadElement.remove();
return formattedFilename;
}

// SAMPLE CSV FORMAT ('content' argument)
Expand All @@ -22,15 +48,14 @@ export default class DownloadService extends Service {
// namespacelonglonglong4/,,191,171,20\n
// namespacelonglonglong4/,auth/method/uMGBU,35,20,15\n'
csv(filename: string, content: string) {
// even though Blob type 'text/csv' is specified below, some browsers (ex. Firefox) require the filename has an explicit extension
const formattedFilename = `${filename?.replace(/\s+/g, '-')}.csv` || 'vault-data.csv';
this.download(formattedFilename, 'text/csv', content);
return formattedFilename;
this.download(filename, content, 'csv');
}

pem(filename: string, content: string) {
const formattedFilename = `${filename?.replace(/\s+/g, '-')}.pem` || 'vault-cert.pem';
this.download(formattedFilename, 'application/x-pem-file', content);
return formattedFilename;
this.download(filename, content, 'pem');
}

miscExtension(filename: string, content: string, extension: string) {
this.download(filename, content, extension);
}
}
3 changes: 1 addition & 2 deletions ui/app/templates/vault/cluster/init.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,8 @@
{{/if}}
<DownloadButton
class="button is-ghost"
@data={{this.keyData}}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

reordered args so that download service and button are consistent: filename, content/data, extension

@filename={{this.keyFilename}}
@mime="application/json"
@data={{this.keyData}}
@extension="json"
@stringify={{true}}
>
Expand Down
2 changes: 1 addition & 1 deletion ui/app/templates/vault/cluster/policy/show.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
<ToolbarActions>
<DownloadButton
class="toolbar-link"
@extension={{if (eq this.policyType "acl") this.model.format "sentinel"}}
@filename={{this.model.name}}
@data={{this.model.policy}}
@extension={{if (eq this.policyType "acl") this.model.format "sentinel"}}
>
Download policy
<Chevron @isButton={{true}} />
Expand Down
49 changes: 23 additions & 26 deletions ui/lib/core/addon/components/download-button.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
/**
* @module DownloadButton
* DownloadButton components are an action button used to download data. Both the action text and icon are yielded.
*
* * NOTE: when using in an engine, remember to add the 'download' service to its dependencies (in /engine.js) and map to it in /app.js
Copy link
Contributor

Choose a reason for hiding this comment

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

❗ great reminder!

* [ember-docs](https://ember-engines.com/docs/services)
* @example
* ```js
* <DownloadButton
Expand All @@ -18,45 +21,39 @@ import Component from '@glimmer/component';
* Download
* </DownloadButton>
* ```
* @param {string} data - data to download
* @param {string} [filename] - name of file that prefixes the ISO timestamp generated at download
* @param {string} [data] - data to download
* @param {string} [extension='txt'] - file extension, the download service uses this to determine the mimetype
* @param {boolean} [stringify=false] - argument to stringify the data before passing to the File constructor
* @param {string} [filename] - name of file that prefixes the ISO timestamp generated when download
* @param {string} [mime='text/plain'] - media type to be downloaded
* @param {string} [extension='txt'] - file extension
*/

export default class DownloadButton extends Component {
get extension() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

wanted order of getters to be consistent and match arg order...

return this.args.extension || 'txt';
}

get mime() {
return this.args.mime || 'text/plain';
}
@service download;
@service flashMessages;

get filename() {
const defaultFilename = `${new Date().toISOString()}.${this.extension}`;
return this.args.filename ? this.args.filename + '-' + defaultFilename : defaultFilename;
const timestamp = new Date().toISOString();
return this.args.filename ? this.args.filename + '-' + timestamp : timestamp;
}

get data() {
get content() {
if (this.args.stringify) {
return JSON.stringify(this.args.data, null, 2);
}
return this.args.data;
}

// TODO refactor and call service instead
get extension() {
return this.args.extension || 'txt';
}

@action
handleDownload() {
const { document, URL } = window;
const downloadElement = document.createElement('a');
const content = new File([this.data], this.filename, { type: this.mime });
downloadElement.download = this.filename;
downloadElement.href = URL.createObjectURL(content);
document.body.appendChild(downloadElement);
downloadElement.click();
URL.revokeObjectURL(downloadElement.href);
downloadElement.remove();
try {
this.download.miscExtension(this.filename, this.content, this.extension);
this.flashMessages.info(`Downloading ${this.filename}`);
} catch (error) {
this.flashMessages.danger(errorMessage(error, 'There was a problem downloading. Please try again.'));
}
}
}
1 change: 1 addition & 0 deletions ui/lib/kmip/addon/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const Eng = Engine.extend({
dependencies: {
services: [
'auth',
'download',
'flash-messages',
'namespace',
'path-help',
Expand Down
2 changes: 1 addition & 1 deletion ui/lib/kmip/addon/templates/configuration.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
{{#if this.model}}
<DownloadButton
class="toolbar-link"
@extension="pem"
@filename={{concat this.model.ca.id "-ca"}}
@data={{this.model.ca.caPem}}
@extension="pem"
>
Download CA cert
<Chevron @isButton={{true}} />
Expand Down
6 changes: 6 additions & 0 deletions ui/lib/pki/addon/components/page/pki-key-details.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
Delete
</ConfirmAction>
<div class="toolbar-separator"></div>
{{#if @key.privateKey}}
<DownloadButton class="toolbar-link" @filename={{this.model.name}} @data={{@key.privateKey}} @extension="pem">
Download private key
<Chevron @isButton={{true}} />
</DownloadButton>
{{/if}}
<ToolbarLink @route="keys.key.edit" @model={{@key.model.name}}>
Edit key
</ToolbarLink>
Expand Down
2 changes: 1 addition & 1 deletion ui/lib/pki/addon/engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export default class PkiEngine extends Engine {
'namespace',
'path-help',
'router',
'secret-mount-path',
'store',
'version',
'wizard',
'secret-mount-path',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

alphabetized? 🤷

],
externalRoutes: ['secrets'],
};
Expand Down
1 change: 0 additions & 1 deletion ui/lib/pki/addon/routes/keys/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { withConfirmLeave } from 'core/decorators/confirm-leave';
@withConfirmLeave()
export default class PkiKeysCreateRoute extends PkiKeysIndexRoute {
@service store;
@service secretMountPath;

model() {
return this.store.createRecord('pki/key');
Expand Down
1 change: 1 addition & 0 deletions ui/tests/acceptance/oidc-auth-method-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ module('Acceptance | oidc auth method', function (hooks) {
cancelTimers();
}, 50);
await click('[data-test-auth-submit]');
await waitUntil(() => find('.nav-user-button button'));
await click('.nav-user-button button');
await click('#logout');
assert
Expand Down