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

PKI Certificate Details #18737

Merged
merged 5 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions ui/app/adapters/pki/certificate.js

This file was deleted.

40 changes: 38 additions & 2 deletions ui/app/adapters/pki/certificate/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,46 @@ import ApplicationAdapter from '../../application';
export default class PkiCertificateBaseAdapter extends ApplicationAdapter {
namespace = 'v1';

deleteRecord(store, type, snapshot) {
getURL(backend, id) {
const uri = `${this.buildURL()}/${encodePath(backend)}`;
return id ? `${uri}/cert/${id}` : `${uri}/certs`;
}

fetchByQuery(query) {
const { backend, id } = query;
const data = !id ? { list: true } : {};
return this.ajax(this.getURL(backend, id), 'GET', { data }).then((resp) => {
resp.data.backend = backend;
if (id) {
resp.data.id = id;
resp.data.serial_number = id;
}
return resp;
});
}

query(store, type, query) {
return this.fetchByQuery(query);
}

queryRecord(store, type, query) {
return this.fetchByQuery(query);
}

// the only way to update a record is by revoking it which will set the revocationTime property
zofskeez marked this conversation as resolved.
Show resolved Hide resolved
updateRecord(store, type, snapshot) {
zofskeez marked this conversation as resolved.
Show resolved Hide resolved
const { backend, serialNumber, certificate } = snapshot.record;
// Revoke certificate requires either serial_number or certificate
const data = serialNumber ? { serial_number: serialNumber } : { certificate };
return this.ajax(`${this.buildURL()}/${encodePath(backend)}/revoke`, 'POST', { data });
return this.ajax(`${this.buildURL()}/${encodePath(backend)}/revoke`, 'POST', { data }).then(
(response) => {
return {
data: {
...this.serialize(snapshot),
...response.data,
},
};
}
);
}
}
18 changes: 14 additions & 4 deletions ui/app/models/pki/certificate/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
* attributes and adapter methods.
*/

const certDisplayFields = ['certificate', 'commonName', 'serialNumber', 'notValidAfter', 'notValidBefore'];
const certDisplayFields = [
'certificate',
'commonName',
'revocationTime',
'issueDate',
'serialNumber',
'notValidBefore',
'notValidAfter',
];

@withFormFields(certDisplayFields)
export default class PkiCertificateBaseModel extends Model {
Expand All @@ -31,16 +39,18 @@ export default class PkiCertificateBaseModel extends Model {

// Attrs that come back from API POST request
@attr() caChain;
@attr('string') certificate;
@attr('string', { masked: true }) certificate;
@attr('number') expiration;
@attr('number', { formatDate: true }) revocationTime;
@attr('string') issuingCa;
@attr('string') privateKey;
@attr('string') privateKeyType;
@attr('string') serialNumber;

// Parsed from cert in serializer
@attr('date') notValidAfter;
@attr('date') notValidBefore;
@attr('number', { formatDate: true }) issueDate;
@attr('number', { formatDate: true }) notValidAfter;
@attr('number', { formatDate: true }) notValidBefore;

// For importing
@attr('string') pemBundle;
Expand Down
30 changes: 30 additions & 0 deletions ui/app/serializers/pki/certificate/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { parseCertificate } from 'vault/helpers/parse-pki-cert';
import ApplicationSerializer from '../../application';

export default class PkiCertificateBaseSerializer extends ApplicationSerializer {
primaryKey = 'serial_number';

attrs = {
role: { serialize: false },
};

normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
// convert issueDate to same format as other date values
if (parsedCert.issue_date) {
parsedCert.issue_date = parsedCert.issue_date.valueOf();
zofskeez marked this conversation as resolved.
Show resolved Hide resolved
}
const json = super.normalizeResponse(
store,
primaryModelClass,
{ ...payload, ...parsedCert },
id,
requestType
);
return json;
}
return super.normalizeResponse(...arguments);
}
}
26 changes: 2 additions & 24 deletions ui/app/serializers/pki/certificate/generate.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
import { parseCertificate } from 'vault/helpers/parse-pki-cert';
import ApplicationSerializer from '../../application';
import PkiCertificateBaseSerializer from './base';

export default class PkiCertificateGenerateSerializer extends ApplicationSerializer {
primaryKey = 'serial_number';
attrs = {
role: { serialize: false },
};

normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (requestType === 'createRecord' && payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
const json = super.normalizeResponse(
store,
primaryModelClass,
{ ...payload, ...parsedCert },
id,
requestType
);
return json;
}
return super.normalizeResponse(...arguments);
}
}
export default class PkiCertificateGenerateSerializer extends PkiCertificateBaseSerializer {}
26 changes: 2 additions & 24 deletions ui/app/serializers/pki/certificate/sign.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,3 @@
import { parseCertificate } from 'vault/helpers/parse-pki-cert';
import ApplicationSerializer from '../../application';
import PkiCertificateBaseSerializer from './base';

export default class PkiCertificateSignSerializer extends ApplicationSerializer {
primaryKey = 'serial_number';
attrs = {
type: { serialize: false },
};

normalizeResponse(store, primaryModelClass, payload, id, requestType) {
if (requestType === 'createRecord' && payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
const json = super.normalizeResponse(
store,
primaryModelClass,
{ ...payload, ...parsedCert },
id,
requestType
);
return json;
}
return super.normalizeResponse(...arguments);
}
}
export default class PkiCertificateGenerateSerializer extends PkiCertificateBaseSerializer {}
55 changes: 55 additions & 0 deletions ui/lib/pki/addon/components/page/pki-certificate-details.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<Toolbar>
<ToolbarActions>
<button type="button" class="toolbar-link" {{on "click" this.downloadCert}} data-test-pki-cert-download-button>
Download
<Chevron @direction="down" @isButton={{true}} />
</button>
{{#if @model.canRevoke}}
<ConfirmAction
@buttonClasses="toolbar-link"
@onConfirmAction={{fn (perform this.revoke)}}
@confirmTitle="Revoke certificate?"
@confirmButtonText="Revoke"
data-test-pki-cert-revoke-button
>
Revoke certificate
</ConfirmAction>
{{/if}}
</ToolbarActions>
</Toolbar>

{{#each @model.formFields as |field|}}
{{#if (eq field.name "certificate")}}
<InfoTableRow @label="Certificate">
<MaskedInput @value={{@model.certificate}} @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
{{else if (eq field.name "serialNumber")}}
<InfoTableRow @label="Serial number">
<code class="has-text-black">{{@model.serialNumber}}</code>
</InfoTableRow>
{{else}}
<InfoTableRow
@label={{capitalize (humanize (dasherize field.name))}}
{{! formatDate fields can be 0 which will cause them to always render -- pass null instead }}
@value={{or (get @model field.name) null}}
@formatDate={{if field.options.formatDate "MMM dd yyyy hh:mm:ss a"}}
@alwaysRender={{false}}
/>
{{/if}}
{{/each}}

{{#if @onBack}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="button"
class="button"
disabled={{this.revoke.isRunning}}
{{on "click" @onBack}}
data-test-pki-cert-details-back
>
Back
</button>
</div>
</div>
{{/if}}
47 changes: 47 additions & 0 deletions ui/lib/pki/addon/components/page/pki-certificate-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
import { task } from 'ember-concurrency';
import { waitFor } from '@ember/test-waiters';
import errorMessage from 'vault/utils/error-message';
import FlashMessageService from 'vault/services/flash-messages';
import DownloadService from 'vault/services/download';
import PkiCertificateBaseModel from 'vault/models/pki/certificate/base';

interface Args {
model: PkiCertificateBaseModel;
onRevoke?: CallableFunction;
onBack?: CallableFunction;
}

export default class PkiCertificateDetailsComponent extends Component<Args> {
@service declare readonly flashMessages: FlashMessageService;
@service declare readonly download: DownloadService;

@action
downloadCert() {
try {
const formattedSerial = this.args.model.serialNumber?.replace(/(\s|:)+/g, '-');
this.download.pem(formattedSerial, this.args.model.certificate);
this.flashMessages.info('Your download has started.');
} catch (err) {
this.flashMessages.danger(errorMessage(err, 'Unable to prepare certificate for download.'));
}
}

@task
@waitFor
*revoke() {
try {
yield this.args.model.save();
zofskeez marked this conversation as resolved.
Show resolved Hide resolved
this.flashMessages.success('The certificate has been revoked.');
if (this.args.onRevoke) {
this.args.onRevoke();
}
} catch (error) {
this.flashMessages.danger(
errorMessage(error, 'Could not revoke certificate. See Vault logs for details.')
);
}
}
}
47 changes: 1 addition & 46 deletions ui/lib/pki/addon/components/pki-role-generate.hbs
Original file line number Diff line number Diff line change
@@ -1,50 +1,5 @@
{{#if @model.serialNumber}}
<Toolbar>
<ToolbarActions>
<button type="button" class="toolbar-link" {{on "click" this.downloadCert}} data-test-pki-cert-download-button>
Download
<Chevron @direction="down" @isButton={{true}} />
</button>
{{#if @model.canRevoke}}
<button
type="button"
class="toolbar-link"
{{on "click" (perform this.revoke)}}
disabled={{this.revoke.isRunning}}
data-test-pki-cert-revoke-button
>
Revoke certificate
<Chevron @direction="right" @isButton={{true}} />
</button>
{{/if}}
</ToolbarActions>
</Toolbar>
{{#each @model.formFields as |attr|}}
{{#if (eq attr.name "certificate")}}
<InfoTableRow @label="Certificate" @value={{@model.certificate}}>
<MaskedInput @value={{@model.certificate}} @name="Certificate" @displayOnly={{true}} @allowCopy={{true}} />
</InfoTableRow>
{{else}}
<InfoTableRow
@label={{or attr.options.label (humanize (dasherize attr.name))}}
@value={{get @model attr.name}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy HH:mm:ss a zzzz"}}
/>
{{/if}}
{{/each}}
<div class="field is-grouped box is-fullwidth is-bottomless">
<div class="control">
<button
type="button"
class="button has-left-margin-s"
disabled={{this.save.isRunning}}
{{on "click" this.cancel}}
data-test-pki-generate-back
>
Back
</button>
</div>
</div>
<Page::PkiCertificateDetails @model={{@model}} @onRevoke={{this.cancel}} @onBack={{this.cancel}} />
{{else}}
<form {{on "submit" (perform this.save)}} data-test-pki-generate-cert-form>
<div class="box is-bottomless is-fullwidth is-marginless">
Expand Down
27 changes: 1 addition & 26 deletions ui/lib/pki/addon/components/pki-role-generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ export default class PkiRoleGenerate extends Component<Args> {

@tracked errorBanner = '';

transitionToRole() {
this.router.transitionTo('vault.cluster.secrets.backend.pki.roles.role.details');
}

get verb() {
return this.args.type === 'sign' ? 'sign' : 'generate';
}
Expand All @@ -45,29 +41,8 @@ export default class PkiRoleGenerate extends Component<Args> {
}
}

@task
*revoke() {
try {
yield this.args.model.destroyRecord();
this.flashMessages.success('The certificate has been revoked.');
this.transitionToRole();
} catch (err) {
this.errorBanner = errorMessage(err, 'Could not revoke certificate. See Vault logs for details.');
}
}

@action downloadCert() {
try {
const formattedSerial = this.args.model.serialNumber?.replace(/(\s|:)+/g, '-');
this.download.pem(formattedSerial, this.args.model.certificate);
this.flashMessages.info('Your download has started.');
} catch (err) {
this.flashMessages.danger(errorMessage(err, 'Unable to prepare certificate for download.'));
}
}

@action cancel() {
this.args.model.unloadRecord();
this.transitionToRole();
this.router.transitionTo('vault.cluster.secrets.backend.pki.roles.role.details');
}
}
Loading