UI: VAULT-8429 Update query and serializer so that it includes parsedCertificate (#20246)
Co-authored-by: Kianna Quach <kianna.quach@hashicorp.com> Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
parent
a592e3a023
commit
8cc9866f10
|
@ -5,6 +5,8 @@
|
|||
|
||||
import ApplicationAdapter from '../application';
|
||||
import { encodePath } from 'vault/utils/path-encoding-helpers';
|
||||
import { all } from 'rsvp';
|
||||
import { verifyCertificates } from 'vault/utils/parse-pki-cert';
|
||||
|
||||
export default class PkiIssuerAdapter extends ApplicationAdapter {
|
||||
namespace = 'v1';
|
||||
|
@ -31,6 +33,18 @@ export default class PkiIssuerAdapter extends ApplicationAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
async getIssuerMetadata(store, type, query, response, id) {
|
||||
const keyInfo = response.data.key_info[id];
|
||||
try {
|
||||
const issuerRecord = await this.queryRecord(store, type, { id, backend: query.backend });
|
||||
const { data } = issuerRecord;
|
||||
const isRoot = await verifyCertificates(data.certificate, data.certificate);
|
||||
return { ...keyInfo, ...data, isRoot };
|
||||
} catch (e) {
|
||||
return { ...keyInfo, issuer_id: id };
|
||||
}
|
||||
}
|
||||
|
||||
updateRecord(store, type, snapshot) {
|
||||
const { issuerId } = snapshot.record;
|
||||
const backend = this._getBackend(snapshot);
|
||||
|
@ -40,11 +54,34 @@ export default class PkiIssuerAdapter extends ApplicationAdapter {
|
|||
}
|
||||
|
||||
query(store, type, query) {
|
||||
return this.ajax(this.urlForQuery(query.backend), 'GET', this.optionsForQuery());
|
||||
const { backend, isListView } = query;
|
||||
const url = this.urlForQuery(backend);
|
||||
|
||||
return this.ajax(url, 'GET', this.optionsForQuery()).then(async (res) => {
|
||||
// To show issuer meta data tags, we have a flag called isListView and only want to
|
||||
// grab each issuer data only if there are less than 10 issuers to avoid making too many requests
|
||||
if (isListView && res.data.keys.length <= 10) {
|
||||
const keyInfoArray = await all(
|
||||
res.data.keys.map((id) => this.getIssuerMetadata(store, type, query, res, id))
|
||||
);
|
||||
const keyInfo = {};
|
||||
|
||||
res.data.keys.forEach((issuerId) => {
|
||||
keyInfo[issuerId] = keyInfoArray.find((newKey) => newKey.issuer_id === issuerId);
|
||||
});
|
||||
|
||||
res.data.key_info = keyInfo;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
queryRecord(store, type, query) {
|
||||
const { backend, id } = query;
|
||||
|
||||
return this.ajax(this.urlForQuery(backend, id), 'GET', this.optionsForQuery(id));
|
||||
}
|
||||
|
||||
|
|
|
@ -44,10 +44,12 @@ export default class PkiIssuerModel extends Model {
|
|||
@attr('string', { label: 'Default key ID', detailLinkTo: 'keys.key.details' }) keyId;
|
||||
@attr({ label: 'CA Chain', masked: true }) caChain;
|
||||
@attr({ masked: true }) certificate;
|
||||
@attr('string') serialNumber;
|
||||
|
||||
// parsed from certificate contents in serializer (see parse-pki-cert.js)
|
||||
@attr parsedCertificate;
|
||||
@attr('string') commonName;
|
||||
@attr isRoot;
|
||||
|
||||
@attr subjectSerialNumber; // this is not the UUID serial number field randomly generated by Vault for leaf certificates
|
||||
@attr({ label: 'Subject Alternative Names (SANs)' }) altNames;
|
||||
|
|
|
@ -16,6 +16,7 @@ export default class PkiIssuerSerializer extends ApplicationSerializer {
|
|||
keyId: { serialize: false },
|
||||
parsedCertificate: { serialize: false },
|
||||
commonName: { serialize: false },
|
||||
isRoot: { serialize: false },
|
||||
};
|
||||
|
||||
normalizeResponse(store, primaryModelClass, payload, id, requestType) {
|
||||
|
@ -37,10 +38,12 @@ export default class PkiIssuerSerializer extends ApplicationSerializer {
|
|||
normalizeItems(payload) {
|
||||
if (payload.data) {
|
||||
if (payload.data?.keys && Array.isArray(payload.data.keys)) {
|
||||
return payload.data.keys.map((issuer_id) => ({
|
||||
issuer_id,
|
||||
...payload.data.key_info[issuer_id],
|
||||
}));
|
||||
return payload.data.keys.map((key) => {
|
||||
return {
|
||||
issuer_id: key,
|
||||
...payload.data.key_info[key],
|
||||
};
|
||||
});
|
||||
}
|
||||
Object.assign(payload, payload.data);
|
||||
delete payload.data;
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
{{#each @issuers as |pkiIssuer idx|}}
|
||||
<LinkedBlock class="list-item-row" @params={{array "issuers.issuer.details" pkiIssuer.id}} @linkPrefix={{@mountPoint}}>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<Icon @name="certificate" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold is-underline">
|
||||
{{pkiIssuer.issuerRef}}
|
||||
{{#if pkiIssuer.issuerName}}
|
||||
<span class="tag has-text-grey-dark">{{pkiIssuer.id}}</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
|
||||
{{#if pkiIssuer.isDefault}}
|
||||
<span class="tag has-text-grey-dark" data-test-is-default={{idx}}>default issuer</span>
|
||||
{{/if}}
|
||||
{{#if (not (eq pkiIssuer.isRoot undefined))}}
|
||||
<span class="tag has-text-grey-dark" data-test-is-root-tag={{idx}}>{{if
|
||||
pkiIssuer.isRoot
|
||||
"root"
|
||||
"intermediate"
|
||||
}}</span>
|
||||
{{/if}}
|
||||
{{#if pkiIssuer.serialNumber}}
|
||||
<InfoTooltip>
|
||||
Serial number
|
||||
</InfoTooltip>
|
||||
<span class="tag has-background-transparent" data-test-serial-number={{idx}}>{{pkiIssuer.serialNumber}}</span>
|
||||
{{/if}}
|
||||
{{#if pkiIssuer.keyId}}
|
||||
<InfoTooltip>
|
||||
Key ID
|
||||
</InfoTooltip>
|
||||
<span class="tag has-background-transparent" data-test-key-id={{idx}}>{{pkiIssuer.keyId}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
<PopupMenu>
|
||||
<nav class="menu" aria-label="issuer config options">
|
||||
<ul class="menu-list">
|
||||
<li data-test-popup-menu-details>
|
||||
<LinkTo @route="issuers.issuer.details" @model={{pkiIssuer.id}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo @route="issuers.issuer.edit" @model={{pkiIssuer.id}}>
|
||||
Edit
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
|
@ -13,7 +13,7 @@ export default class PkiIssuersListRoute extends Route {
|
|||
|
||||
model() {
|
||||
return this.store
|
||||
.query('pki/issuer', { backend: this.secretMountPath.currentPath })
|
||||
.query('pki/issuer', { backend: this.secretMountPath.currentPath, isListView: true })
|
||||
.then((issuersModel) => {
|
||||
return { issuersModel, parentModel: this.modelFor('issuers') };
|
||||
})
|
||||
|
|
|
@ -45,53 +45,9 @@
|
|||
</BasicDropdown>
|
||||
</ToolbarActions>
|
||||
</Toolbar>
|
||||
|
||||
{{#if this.model.issuersModel.length}}
|
||||
{{#each this.model.issuersModel as |pkiIssuer|}}
|
||||
<LinkedBlock
|
||||
class="list-item-row"
|
||||
@params={{array "issuers.issuer.details" pkiIssuer.id}}
|
||||
@linkPrefix={{this.mountPoint}}
|
||||
>
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left">
|
||||
<div>
|
||||
<Icon @name="certificate" class="has-text-grey-light" />
|
||||
<span class="has-text-weight-semibold is-underline">
|
||||
{{pkiIssuer.issuerRef}}
|
||||
</span>
|
||||
<div class="is-flex-row has-left-margin-l has-top-margin-xs">
|
||||
{{#if pkiIssuer.isDefault}}
|
||||
<span class="tag has-text-grey-dark">default issuer</span>
|
||||
{{/if}}
|
||||
{{#if pkiIssuer.issuerName}}
|
||||
<span class="tag has-text-grey-dark">{{pkiIssuer.id}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right is-flex is-paddingless is-marginless">
|
||||
<div class="level-item">
|
||||
<PopupMenu>
|
||||
<nav class="menu" aria-label="issuer config options">
|
||||
<ul class="menu-list">
|
||||
<li data-test-popup-menu-details>
|
||||
<LinkTo @route="issuers.issuer.details" @model={{pkiIssuer.id}}>
|
||||
Details
|
||||
</LinkTo>
|
||||
</li>
|
||||
<li>
|
||||
<LinkTo @route="issuers.issuer.edit" @model={{pkiIssuer.id}}>
|
||||
Edit
|
||||
</LinkTo>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LinkedBlock>
|
||||
{{/each}}
|
||||
<Page::PkiIssuerList @issuers={{this.model.issuersModel}} @mountPoint={{this.mountPoint}} />
|
||||
{{else}}
|
||||
<EmptyState @title="PKI not configured" @message={{this.notConfiguredMessage}}>
|
||||
<LinkTo @route="configuration.create">
|
||||
|
|
|
@ -340,9 +340,48 @@ module('Acceptance | pki workflow', function (hooks) {
|
|||
hooks.beforeEach(async function () {
|
||||
await authPage.login();
|
||||
// Configure engine with a default issuer
|
||||
await runCommands([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]);
|
||||
await runCommands([
|
||||
`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test" name="Hashicorp Test"`,
|
||||
]);
|
||||
await logout.visit();
|
||||
});
|
||||
test('lists the correct issuer metadata info', async function (assert) {
|
||||
assert.expect(6);
|
||||
await authPage.login(this.pkiAdminToken);
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
assert.dom(SELECTORS.issuersTab).exists('Issuers tab is present');
|
||||
await click(SELECTORS.issuersTab);
|
||||
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers`);
|
||||
assert.dom('.linked-block').exists({ count: 1 }, 'One issuer is in list');
|
||||
assert.dom('[data-test-is-root-tag="0"]').hasText('root');
|
||||
assert.dom('[data-test-serial-number="0"]').exists({ count: 1 }, 'displays serial number tag');
|
||||
assert.dom('[data-test-key-id="0"]').exists({ count: 1 }, 'displays key id tag');
|
||||
});
|
||||
test('lists the correct issuer metadata info when user has only read permission', async function (assert) {
|
||||
assert.expect(2);
|
||||
await authPage.login();
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
await click(SELECTORS.issuersTab);
|
||||
await click(SELECTORS.issuerPopupMenu);
|
||||
await click(SELECTORS.issuerPopupDetails);
|
||||
const issuerId = find(SELECTORS.issuerDetails.valueByName('Issuer ID')).innerText;
|
||||
const pki_issuer_denied_policy = `
|
||||
path "${this.mountPath}/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
},
|
||||
path "${this.mountPath}/issuer/${issuerId}" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
`;
|
||||
this.token = await tokenWithPolicy('pki-issuer-denied-policy', pki_issuer_denied_policy);
|
||||
await logout.visit();
|
||||
await authPage.login(this.token);
|
||||
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
|
||||
await click(SELECTORS.issuersTab);
|
||||
assert.dom('[data-test-serial-number="0"]').exists({ count: 1 }, 'displays serial number tag');
|
||||
assert.dom('[data-test-key-id="0"]').exists({ count: 1 }, 'displays key id tag');
|
||||
});
|
||||
|
||||
test('details view renders correct number of info items', async function (assert) {
|
||||
assert.expect(13);
|
||||
await authPage.login(this.pkiAdminToken);
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { setupEngine } from 'ember-engines/test-support';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
|
||||
/**
|
||||
* this test is for the page component only. A separate test is written for the form rendered
|
||||
*/
|
||||
module('Integration | Component | page/pki-issuer-list', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupEngine(hooks, 'pki');
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||
this.secretMountPath.currentPath = 'pki-test';
|
||||
this.engineId = 'pki';
|
||||
});
|
||||
|
||||
test('it renders when issuer metadata tags when info is provided', async function (assert) {
|
||||
this.store.createRecord('pki/issuer', {
|
||||
issuerId: 'abcd-efgh',
|
||||
issuerName: 'issuer-0',
|
||||
common_name: 'common-name-issuer-0',
|
||||
isRoot: true,
|
||||
serialNumber: '74:2d:ed:f2:c4:3b:76:5e:6e:0d:f1:6a:c0:8b:6f:e3:3c:62:f9:03',
|
||||
});
|
||||
this.store.createRecord('pki/issuer', {
|
||||
issuerId: 'ijkl-mnop',
|
||||
issuerName: 'issuer-1',
|
||||
isRoot: false,
|
||||
parsedCertificate: {
|
||||
common_name: 'common-name-issuer-1',
|
||||
},
|
||||
serialNumber: '74:2d:ed:f2:c4:3b:76:5e:6e:0d:f1:6a:c0:8b:6f:e3:3c:62:f9:03',
|
||||
});
|
||||
this.issuers = this.store.peekAll('pki/issuer');
|
||||
|
||||
await render(hbs`<Page::PkiIssuerList @issuers={{this.issuers}} @mountPoint={{this.engineId}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
|
||||
this.issuers.forEach(async (issuer, idx) => {
|
||||
assert
|
||||
.dom(`[data-test-serial-number="${idx}"]`)
|
||||
.hasText('74:2d:ed:f2:c4:3b:76:5e:6e:0d:f1:6a:c0:8b:6f:e3:3c:62:f9:03');
|
||||
if (idx === 1) {
|
||||
assert.dom(`[data-test-is-root-tag="${idx}"]`).hasText('intermediate');
|
||||
} else {
|
||||
assert.dom(`[data-test-is-root-tag="${idx}"]`).hasText('root');
|
||||
}
|
||||
});
|
||||
});
|
||||
test('it renders when issuer data even though issuer metadata isnt provided', async function (assert) {
|
||||
this.store.createRecord('pki/issuer', {
|
||||
issuerId: 'abcd-efgh',
|
||||
issuerName: 'issuer-0',
|
||||
isDefault: false,
|
||||
});
|
||||
this.store.createRecord('pki/issuer', {
|
||||
issuerId: 'ijkl-mnop',
|
||||
issuerName: 'issuer-1',
|
||||
isDefault: true,
|
||||
});
|
||||
this.issuers = this.store.peekAll('pki/issuer');
|
||||
|
||||
await render(hbs`<Page::PkiIssuerList @issuers={{this.issuers}} @mountPoint={{this.engineId}} />`, {
|
||||
owner: this.engine,
|
||||
});
|
||||
assert.dom(`[data-test-is-default="1"]`).hasText('default issuer');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue